redstruct 0.1.7 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -11
- data/Rakefile +5 -5
- data/lib/redstruct/all.rb +14 -0
- data/lib/redstruct/configuration.rb +9 -6
- data/lib/redstruct/connection_proxy.rb +123 -0
- data/lib/redstruct/counter.rb +96 -0
- data/lib/redstruct/error.rb +2 -0
- data/lib/redstruct/factory/object.rb +31 -0
- data/lib/redstruct/factory.rb +94 -55
- data/lib/redstruct/hash.rb +123 -0
- data/lib/redstruct/list.rb +315 -0
- data/lib/redstruct/lock.rb +183 -0
- data/lib/redstruct/script.rb +104 -0
- data/lib/redstruct/set.rb +155 -0
- data/lib/redstruct/sorted_set/slice.rb +124 -0
- data/lib/redstruct/sorted_set.rb +153 -0
- data/lib/redstruct/string.rb +66 -0
- data/lib/redstruct/struct.rb +87 -0
- data/lib/redstruct/utils/coercion.rb +14 -8
- data/lib/redstruct/utils/inspectable.rb +8 -4
- data/lib/redstruct/utils/iterable.rb +52 -0
- data/lib/redstruct/utils/scriptable.rb +32 -6
- data/lib/redstruct/version.rb +4 -1
- data/lib/redstruct.rb +17 -51
- data/lib/yard/defscript_handler.rb +5 -3
- data/test/redstruct/configuration_test.rb +13 -0
- data/test/redstruct/connection_proxy_test.rb +85 -0
- data/test/redstruct/counter_test.rb +108 -0
- data/test/redstruct/factory/object_test.rb +21 -0
- data/test/redstruct/factory_test.rb +136 -0
- data/test/redstruct/hash_test.rb +138 -0
- data/test/redstruct/list_test.rb +244 -0
- data/test/redstruct/lock_test.rb +108 -0
- data/test/redstruct/script_test.rb +53 -0
- data/test/redstruct/set_test.rb +219 -0
- data/test/redstruct/sorted_set/slice_test.rb +10 -0
- data/test/redstruct/sorted_set_test.rb +219 -0
- data/test/redstruct/string_test.rb +8 -0
- data/test/redstruct/struct_test.rb +61 -0
- data/test/redstruct/utils/coercion_test.rb +33 -0
- data/test/redstruct/utils/inspectable_test.rb +31 -0
- data/test/redstruct/utils/iterable_test.rb +94 -0
- data/test/redstruct/utils/scriptable_test.rb +67 -0
- data/test/redstruct_test.rb +14 -0
- data/test/test_helper.rb +77 -1
- metadata +58 -26
- data/lib/redstruct/connection.rb +0 -47
- data/lib/redstruct/factory/creation.rb +0 -95
- data/lib/redstruct/factory/deserialization.rb +0 -7
- data/lib/redstruct/hls/lock.rb +0 -175
- data/lib/redstruct/hls/queue.rb +0 -29
- data/lib/redstruct/hls.rb +0 -2
- data/lib/redstruct/types/base.rb +0 -36
- data/lib/redstruct/types/counter.rb +0 -65
- data/lib/redstruct/types/hash.rb +0 -72
- data/lib/redstruct/types/list.rb +0 -76
- data/lib/redstruct/types/script.rb +0 -56
- data/lib/redstruct/types/set.rb +0 -96
- data/lib/redstruct/types/sorted_set.rb +0 -129
- data/lib/redstruct/types/string.rb +0 -64
- data/lib/redstruct/types/struct.rb +0 -58
- data/lib/releaser/logger.rb +0 -15
- data/lib/releaser/repository.rb +0 -32
- data/lib/tasks/release.rake +0 -49
- data/test/redstruct/restruct_test.rb +0 -4
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redstruct
|
4
|
+
module Utils
|
5
|
+
# Adds iterable capabilities to any object which implements a to_enum method with the correct arguments.
|
6
|
+
module Iterable
|
7
|
+
# Iterates over the keys of this factory using one of the redis scan commands (scan, zscan, hscan, sscan)
|
8
|
+
# For more about the scan command, see https://redis.io/commands/scan
|
9
|
+
# @param [String] match will prepend the factory namespace to the match string; see the redis documentation for the syntax
|
10
|
+
# @param [Integer] count maximum number of items returned per scan command (see Redis internals)
|
11
|
+
# @param [Integer] max_iterations maximum number of iterations; if nil given, could potentially never terminate
|
12
|
+
# @param [Integer] batch_size if greater than 1, will yield arrays of keys of size between 1 and batch_size
|
13
|
+
# @return [Enumerator] if no block given, returns an enumerator that you can chain with others
|
14
|
+
def each(match: '*', count: 10, max_iterations: 10_000, batch_size: 1, **options, &block) # rubocop: disable Metrics/ParameterLists
|
15
|
+
enumerator = to_enum(match: match, count: count, **options)
|
16
|
+
enumerator = enumerator.each_slice(batch_size) if batch_size > 1
|
17
|
+
enumerator = Redstruct::Utils::Iterable.bound_enumerator(enumerator, max: max_iterations) unless max_iterations.nil?
|
18
|
+
|
19
|
+
return enumerator unless block_given?
|
20
|
+
return enumerator.each(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Including classes should overload this class to provide an initial enumerator
|
24
|
+
# NOTE: to namespace the matcher (which you should), use `@factory.prefix(match)`
|
25
|
+
# @param [String] match see the redis documentation for the syntax
|
26
|
+
# @param [Integer] count number of keys fetched from redis per scan command (NOTE the enum still passes each keys 1 by 1)
|
27
|
+
def to_enum(match: '*', count: 10) # rubocop: disable Lint/UnusedMethodArgument
|
28
|
+
raise NotImplementedError.new, 'including classes should overload to_enum'
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# Returns an enumerator which limits the maximum number of iterations
|
33
|
+
# possible on another enumerator.
|
34
|
+
# @param [Enumerator] enumerator the unbounded enumerator to wrap
|
35
|
+
# @param [Integer] max maximum number of iterations possible
|
36
|
+
# @return [Enumerator]
|
37
|
+
def bound_enumerator(enumerator, max:)
|
38
|
+
raise ArgumentError, 'max must be greater than 0' unless max.positive?
|
39
|
+
|
40
|
+
return Enumerator.new do |yielder|
|
41
|
+
iterations = 0
|
42
|
+
loop do
|
43
|
+
yielder << enumerator.next
|
44
|
+
iterations += 1
|
45
|
+
raise StopIteration if iterations == max
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,18 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
|
1
5
|
module Redstruct
|
2
6
|
module Utils
|
7
|
+
# Provides utility methods to add lua scripts to any class
|
3
8
|
module Scriptable
|
9
|
+
# Callback called whenever the module is included. Adds all methods under ClassMethods as class methods of the
|
10
|
+
# includer.
|
4
11
|
def self.included(base)
|
5
12
|
base.extend(ClassMethods)
|
6
13
|
end
|
7
14
|
|
15
|
+
# Class methods added when the module is included at the class level (i.e. extend)
|
8
16
|
module ClassMethods
|
9
|
-
|
10
|
-
|
17
|
+
# Creates a method with the given id, which will create a constant and a method in the class. This allows you
|
18
|
+
# to use defscript as a macro for your lua scripts, which gets translated to Ruby code at compile time.
|
19
|
+
# @param [String] id the script ID
|
20
|
+
# @param [String] script the lua script source
|
21
|
+
def defscript(id, script)
|
22
|
+
raise ArgumentError, 'no script given' unless script && !script.empty?
|
23
|
+
|
24
|
+
script = script.strip
|
25
|
+
constant = "SCRIPT_#{id.upcase}"
|
26
|
+
|
27
|
+
if const_defined?(constant)
|
28
|
+
Redstruct.logger.warn("cowardly aborting defscript #{id}; constant with name #{constant} already exists!")
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
if method_defined?(id)
|
33
|
+
Redstruct.logger.warn("cowardly aborting defscript #{id}; method with name #{id} already exists!")
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
11
37
|
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
38
|
+
#{constant} = { script: %(#{script}).freeze, sha1: Digest::SHA1.hexdigest(%(#{script})).freeze }.freeze
|
39
|
+
def #{id}(keys: [], argv: [])
|
40
|
+
return @factory.script(#{constant}[:script], sha1: #{constant}[:sha1]).eval(keys: keys, argv: argv)
|
41
|
+
end
|
16
42
|
METHOD
|
17
43
|
end
|
18
44
|
end
|
data/lib/redstruct/version.rb
CHANGED
data/lib/redstruct.rb
CHANGED
@@ -1,65 +1,31 @@
|
|
1
|
-
#
|
2
|
-
require 'redis'
|
3
|
-
require 'connection_pool'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
require 'redstruct/version'
|
7
|
-
require 'redstruct/utils/inspectable'
|
8
|
-
require 'redstruct/utils/scriptable'
|
9
|
-
require 'redstruct/utils/coercion'
|
10
|
-
|
11
|
-
# Core
|
12
|
-
require 'redstruct/connection'
|
3
|
+
require 'logger'
|
13
4
|
require 'redstruct/configuration'
|
14
|
-
require 'redstruct/error'
|
15
|
-
require 'redstruct/types/base'
|
16
|
-
|
17
|
-
# Factory
|
18
|
-
require 'redstruct/factory/creation'
|
19
|
-
require 'redstruct/factory/deserialization'
|
20
5
|
require 'redstruct/factory'
|
21
6
|
|
22
|
-
#
|
23
|
-
|
24
|
-
require 'redstruct/types/string'
|
25
|
-
require 'redstruct/types/counter'
|
26
|
-
require 'redstruct/types/hash'
|
27
|
-
require 'redstruct/types/list'
|
28
|
-
require 'redstruct/types/script'
|
29
|
-
require 'redstruct/types/set'
|
30
|
-
require 'redstruct/types/sorted_set'
|
31
|
-
|
7
|
+
# Top level namespace
|
8
|
+
# TODO: Add documentation later
|
32
9
|
module Redstruct
|
33
10
|
class << self
|
11
|
+
# @return [Redstruct::Configuration] current default configuration
|
34
12
|
def config
|
35
|
-
return @config ||= Configuration.new
|
13
|
+
return @config ||= Redstruct::Configuration.new
|
36
14
|
end
|
37
15
|
|
38
|
-
|
39
|
-
|
16
|
+
# The current logger; if nil, will lazily create a default logger (STDOUT, WARN)
|
17
|
+
# @return [Logger] current logger
|
18
|
+
def logger
|
19
|
+
return @logger ||= default_logger
|
40
20
|
end
|
21
|
+
attr_writer :logger
|
41
22
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
return
|
47
|
-
end
|
48
|
-
|
49
|
-
def []=(key, factory)
|
50
|
-
if factory.nil?
|
51
|
-
factories.delete(key)
|
52
|
-
else
|
53
|
-
factories[key] = factory
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def make(name: nil, pool: nil, namespace: nil)
|
58
|
-
factory = Redstruct::Factory.new(pool: pool, namespace: namespace)
|
59
|
-
name = Redstruct if name.nil?
|
60
|
-
self[name] = factory unless name.nil?
|
61
|
-
|
62
|
-
return factory
|
23
|
+
def default_logger
|
24
|
+
logger = Logger.new(STDOUT)
|
25
|
+
logger.level = Logger::WARN
|
26
|
+
logger.progname = 'Redstruct'
|
27
|
+
return logger
|
63
28
|
end
|
29
|
+
private :default_logger
|
64
30
|
end
|
65
31
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module YARD
|
4
|
+
class DefscriptHandler < YARD::Handlers::Ruby::Base # :nodoc:
|
5
|
+
GROUP_NAME = 'Lua Scripts'
|
4
6
|
|
5
7
|
handles method_call(:defscript)
|
6
8
|
namespace_only
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
class ConfigurationTest < Redstruct::Test
|
7
|
+
def test_initialize
|
8
|
+
@config = Redstruct::Configuration.new
|
9
|
+
assert_nil @config.default_connection, 'Should have no default connection initially'
|
10
|
+
assert_nil @config.default_namespace, 'Should have no namespace initially'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
# TODO: not quite sure if this test suite is good enough
|
7
|
+
class ConnectionProxyTest < Redstruct::Test
|
8
|
+
def test_initialize
|
9
|
+
assert_raises(ArgumentError, 'should fail to initialize without a proxy object') { Redstruct::ConnectionProxy.new }
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_connection
|
13
|
+
proxy = connection_proxy(redis_connection)
|
14
|
+
assert_equal 'PONG', proxy.ping, 'Should correctly execute the ping command using the given redis connection'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_connection_pool
|
18
|
+
proxy = connection_proxy
|
19
|
+
assert_equal 'PONG', proxy.ping, 'Should correctly execute the ping command using the given connection pool'
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_with
|
23
|
+
proxy = connection_proxy
|
24
|
+
proxy.with do |connection|
|
25
|
+
assert_kind_of Redis, connection, 'should have yielded a redis connection'
|
26
|
+
proxy.with do |new_connection|
|
27
|
+
assert_equal connection, new_connection, 'calling with from within a with block should return the same connection'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
connection = redis_connection
|
32
|
+
proxy = connection_proxy(connection)
|
33
|
+
proxy.with do |new_connection|
|
34
|
+
assert_equal connection, new_connection, 'calling with when proxying a single connection should return that connection'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# it is easier to test with a mocked redis connection than with a connection pool, and as far as I can tell, has
|
39
|
+
# no downsides
|
40
|
+
def test_proxied_methods
|
41
|
+
connection = flexmock(redis_connection)
|
42
|
+
proxy = connection_proxy(connection)
|
43
|
+
|
44
|
+
proxied_methods = Redis.public_instance_methods(false) - Redstruct::ConnectionProxy::NON_COMMAND_METHODS
|
45
|
+
proxied_methods.each do |method|
|
46
|
+
retval = SecureRandom.hex(8)
|
47
|
+
args = generate_random_args
|
48
|
+
connection.should_receive(method).with(*args, Proc).and_return(retval).once
|
49
|
+
assert_equal retval, proxy.public_send(method, *args) {}, "#{method} should be proxied with the correct arguments and block"
|
50
|
+
end
|
51
|
+
|
52
|
+
# flexmock automatically verifies all expected calls were matched
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_method_missing
|
56
|
+
connection = flexmock(redis_connection)
|
57
|
+
proxy = connection_proxy(connection)
|
58
|
+
|
59
|
+
method = '__strange_method__'
|
60
|
+
connection.should_receive(method).and_return(42)
|
61
|
+
assert_equal 42, proxy.public_send(method), 'Should proxy even missing methods to the connection object'
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_respond_to_missing?
|
65
|
+
assert connection_proxy.respond_to?('__strange_method__'), 'Redis class responds to all missing method, and so should ConnectionProxy'
|
66
|
+
end
|
67
|
+
|
68
|
+
def connection_proxy(connection = nil)
|
69
|
+
connection ||= Redstruct.config.default_connection
|
70
|
+
return Redstruct::ConnectionProxy.new(connection)
|
71
|
+
end
|
72
|
+
|
73
|
+
# obtain a plain redis connection
|
74
|
+
def redis_connection
|
75
|
+
return Redstruct.config.default_connection.with { |c| c }.dup
|
76
|
+
end
|
77
|
+
private :redis_connection
|
78
|
+
|
79
|
+
def generate_random_args
|
80
|
+
argc = SecureRandom.random_number(10) + 1
|
81
|
+
return Array.new(argc) { SecureRandom.hex(4) }
|
82
|
+
end
|
83
|
+
private :generate_random_args
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
class CounterTest < Redstruct::Test
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
@factory = create_factory
|
10
|
+
@counter = @factory.counter('counter')
|
11
|
+
@ring = @factory.counter('ring', max: 4)
|
12
|
+
@pair = @factory.counter('pair', by: 2)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initialize
|
16
|
+
assert_equal 1, @counter.default_increment, 'should have default increment'
|
17
|
+
assert_nil @counter.max, 'should have no maximum value by default'
|
18
|
+
|
19
|
+
assert_equal 2, @pair.default_increment, 'should increment by 2'
|
20
|
+
assert_nil @pair.max, 'should have no maximum value'
|
21
|
+
|
22
|
+
assert_equal 1, @ring.default_increment, 'should have a default increment'
|
23
|
+
assert_equal 4, @ring.max, 'should have a maximum value'
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_get
|
27
|
+
assert_equal 0, @counter.get, 'initial value is always nil'
|
28
|
+
@counter.set(1)
|
29
|
+
assert_equal 1, @counter.get, 'should be now be equal to 1'
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_set
|
33
|
+
assert_equal 0, @counter.get, 'initial value is always nil'
|
34
|
+
assert @counter.set(2), 'should return true has it has been set'
|
35
|
+
assert_equal 2, @counter.get, 'should be now be equal to 2 after a set'
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_getset
|
39
|
+
assert_equal 0, @counter.getset(2), 'should return the old value (0)'
|
40
|
+
assert_equal 2, @counter.getset(4), 'should return the old value (2)'
|
41
|
+
assert_equal 4, @counter.get, 'should return the current value'
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_increment
|
45
|
+
assert_equal 1, @counter.increment, 'should increment by 1 and return the value'
|
46
|
+
assert_equal 2, @counter.increment, 'should increment by 1 and return the value'
|
47
|
+
|
48
|
+
assert_equal 2, @pair.increment, 'should increment by 2 and return the value'
|
49
|
+
assert_equal 4, @pair.increment, 'should increment by 2 and return the value'
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_increment_by
|
53
|
+
assert_equal 2, @counter.increment(by: 2), 'should increment by 2 and return the new value'
|
54
|
+
assert_equal 3, @counter.increment, 'should increment by 1 and return the new value'
|
55
|
+
|
56
|
+
assert_equal 3, @pair.increment(by: 3), 'should increment by 3 and return the new value'
|
57
|
+
assert_equal 5, @pair.increment, 'should increment by 2 and return the new value'
|
58
|
+
|
59
|
+
assert_equal 2, @ring.increment(by: 2), 'should increment by 2 and return the new value'
|
60
|
+
assert_equal 3, @ring.increment, 'should increment by 1 and return the new value'
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_increment_ring
|
64
|
+
assert_equal 2, @counter.increment(by: 2, max: 3), 'should increment by 2 and return the new value'
|
65
|
+
assert_equal 0, @counter.increment(max: 3), 'should increment by 1, cycle around, and return the new value'
|
66
|
+
|
67
|
+
assert_equal 0, @pair.increment(max: 2), 'should increment by 2, cycle around, and return the new value'
|
68
|
+
assert_equal 0, @pair.increment(max: 2), 'should increment by 2, cycle around, and return the new value'
|
69
|
+
|
70
|
+
assert_equal 2, @ring.increment(by: 2), 'should increment by 2 and return the new value'
|
71
|
+
assert_equal 0, @ring.increment(by: 2), 'should increment by 2, cycle around, and return the new value'
|
72
|
+
assert_equal 1, @ring.increment(by: 5), 'should increment by 2, cycle around, and return the new value'
|
73
|
+
assert_equal 6, @ring.increment(by: 5, max: 7), 'should increment by 2, cycle around, and return the new value'
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_decrement
|
77
|
+
assert_equal(-1, @counter.decrement, 'should decrement by 1 and return the value')
|
78
|
+
assert_equal(-2, @counter.decrement, 'should decrement by 1 and return the value')
|
79
|
+
|
80
|
+
assert_equal(-2, @pair.decrement, 'should decrement by 2 and return the value')
|
81
|
+
assert_equal(-4, @pair.decrement, 'should decrement by 2 and return the value')
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_decrement_by
|
85
|
+
assert_equal(-2, @counter.decrement(by: 2), 'should decrement by 2 and return the new value')
|
86
|
+
assert_equal(-3, @counter.decrement, 'should decrement by 1 and return the new value')
|
87
|
+
|
88
|
+
assert_equal(-3, @pair.decrement(by: 3), 'should decrement by 3 and return the new value')
|
89
|
+
assert_equal(-5, @pair.decrement, 'should decrement by 2 and return the new value')
|
90
|
+
|
91
|
+
assert_equal(-2, @ring.decrement(by: 2), 'should decrement by 2 and return the new value')
|
92
|
+
assert_equal(-3, @ring.decrement, 'should decrement by 1 and return the new value')
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_decrement_ring
|
96
|
+
assert_equal(-2, @counter.decrement(by: 2, max: 3), 'should decrement by 2 and return the new value')
|
97
|
+
assert_equal 0, @counter.decrement(max: 3), 'should decrement by 1, cycle around, and return the new value'
|
98
|
+
|
99
|
+
assert_equal 0, @pair.decrement(max: 2), 'should decrement by 2, cycle around, and return the new value'
|
100
|
+
assert_equal 0, @pair.decrement(max: 2), 'should decrement by 2, cycle around, and return the new value'
|
101
|
+
|
102
|
+
assert_equal(-2, @ring.decrement(by: 2), 'should decrement by 2 and return the new value')
|
103
|
+
assert_equal 0, @ring.decrement(by: 2), 'should decrement by 2, cycle around, and return the new value'
|
104
|
+
assert_equal(-1, @ring.decrement(by: 5), 'should decrement by 2, cycle around, and return the new value')
|
105
|
+
assert_equal(-6, @ring.decrement(by: 5, max: 7), 'should decrement by 2, cycle around, and return the new value')
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
class Factory
|
7
|
+
class ObjectTest < Redstruct::Test
|
8
|
+
def test_initialize
|
9
|
+
factory = create_factory
|
10
|
+
object = Redstruct::Factory::Object.new(factory: factory)
|
11
|
+
assert_equal factory, object.factory, 'should have the given factory as the object factory'
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_connection
|
15
|
+
factory = create_factory
|
16
|
+
object = Redstruct::Factory::Object.new(factory: factory)
|
17
|
+
assert_equal factory.connection, object.connection, 'should delegate #connection to the factory connection'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module Redstruct
|
6
|
+
class FactoryTest < Redstruct::Test
|
7
|
+
def test_initialize_default
|
8
|
+
flexmock(Redstruct::ConnectionProxy).should_receive(:new).with(Redstruct.config.default_connection).once.pass_thru
|
9
|
+
factory = Redstruct::Factory.new
|
10
|
+
|
11
|
+
refute_nil factory.connection, 'should have the default connection when none provided'
|
12
|
+
assert_equal Redstruct.config.default_namespace, factory.namespace, 'should be the default namespace if none provided'
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_initialize_params
|
16
|
+
namespace = 'test'
|
17
|
+
connection = Redstruct::ConnectionProxy.new(ConnectionPool.new(size: 1, timeout: 1) {}) # for the purpose of the test, does not matter what the pool creates
|
18
|
+
factory = Redstruct::Factory.new(connection: connection, namespace: namespace)
|
19
|
+
|
20
|
+
assert_equal namespace, factory.namespace, 'should have assigned the correct namespace'
|
21
|
+
assert_kind_of Redstruct::ConnectionProxy, factory.connection, 'should have properly constructed the proxy'
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_initialize_no_proxy
|
25
|
+
assert_raises(ArgumentError, 'should fail when no connection proxy given') { Redstruct::Factory.new(connection: 'test') }
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_prefix_no_namespace
|
29
|
+
factory = Redstruct::Factory.new(namespace: '')
|
30
|
+
assert_equal 'key', factory.prefix('key'), 'should not namespace when namespace is blank string'
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_prefix
|
34
|
+
factory = Redstruct::Factory.new(namespace: 'test')
|
35
|
+
assert_equal 'test:key', factory.prefix('key'), 'should correctly prefix the key with a namespace'
|
36
|
+
assert_equal 'test:key', factory.prefix('test:key'), 'should not prefix an already prefixed key'
|
37
|
+
assert_equal 'test:testkey', factory.prefix('testkey'), 'should prefix even if the key starts with the namespace (but is not separated by a colon)'
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_to_enum
|
41
|
+
factory, keys = populated_factory
|
42
|
+
enum = factory.to_enum
|
43
|
+
# wrap in set since we don't care about the ordering, and the redis scan command can theoretically return a key
|
44
|
+
# more than once
|
45
|
+
assert_equal ::Set.new(keys), ::Set.new(enum.to_a), 'Should retrieve all keys'
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_to_enum_match
|
49
|
+
factory, keys = populated_factory
|
50
|
+
|
51
|
+
expected_key = keys[0]
|
52
|
+
pattern = expected_key + '*'
|
53
|
+
|
54
|
+
enum = factory.to_enum(match: pattern)
|
55
|
+
retrieved = enum.to_a
|
56
|
+
assert_equal 1, retrieved.size, 'should have retrieved only one key'
|
57
|
+
assert_equal expected_key, retrieved[0], 'should have retrieved the correct key'
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_delete
|
61
|
+
factory, = populated_factory
|
62
|
+
keys_matcher = factory.prefix('*')
|
63
|
+
|
64
|
+
refute_empty factory.connection.keys(keys_matcher), 'should have at least one key in the factory, otherwise test is pointless'
|
65
|
+
factory.delete
|
66
|
+
assert_empty factory.connection.keys(keys_matcher), 'should have no more keys in the factory'
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_script
|
70
|
+
factory = create_factory
|
71
|
+
script = factory.script('return 0')
|
72
|
+
assert_kind_of Redstruct::Script, script, 'should always return a script object'
|
73
|
+
assert_equal 0, script.eval, 'should execute script correctly'
|
74
|
+
assert_equal factory.connection, script.connection, 'script and factory should share the same connection'
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_factory
|
78
|
+
factory = create_factory
|
79
|
+
sub_factory = factory.factory('sub')
|
80
|
+
|
81
|
+
assert sub_factory.namespace.start_with?(factory.namespace), 'should be prefixed with parent factory namespace'
|
82
|
+
assert_equal factory.connection, sub_factory.connection, 'should share the same connection'
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_lock
|
86
|
+
factory1 = create_factory
|
87
|
+
lock1 = factory1.lock('res')
|
88
|
+
assert_kind_of Redstruct::Lock, lock1, 'should always return a Redstruct::Lock'
|
89
|
+
|
90
|
+
factory2 = create_factory
|
91
|
+
lock2 = factory2.lock('res')
|
92
|
+
|
93
|
+
assert lock1.acquire, 'should be able to acquire free lock'
|
94
|
+
assert lock2.acquire, 'should be able to acquire lock for the a resource with the same name but living in a different factory (i.e. not the same resource!)'
|
95
|
+
refute factory1.lock('res').acquire, 'should not be able to acquire lock on pre-acquired resource'
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_hashmap
|
99
|
+
factory = create_factory
|
100
|
+
assert_struct_method(:hashmap, Redstruct::Hash, factory)
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_structs
|
104
|
+
factory = create_factory
|
105
|
+
%w[Counter List Set SortedSet String Struct].each do |struct|
|
106
|
+
method = struct.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
107
|
+
type = Redstruct.const_get(struct)
|
108
|
+
assert_struct_method(method, type, factory)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def assert_struct_method(method, type, factory)
|
113
|
+
assert Redstruct::Factory.method_defined?(method), "factory should have a method for #{type} named #{method}"
|
114
|
+
|
115
|
+
object = factory.public_send(method, 'key')
|
116
|
+
assert_equal factory.prefix('key'), object.key, 'object key should be namespaced under the factory namespace'
|
117
|
+
assert_equal factory, object.factory
|
118
|
+
assert_kind_of type, object, "factory method #{method} should always return an object of type #{type}"
|
119
|
+
end
|
120
|
+
private :assert_struct_method
|
121
|
+
|
122
|
+
def populated_factory
|
123
|
+
factory = create_factory
|
124
|
+
|
125
|
+
# populate between 2 and 10 objects, random keys, random values
|
126
|
+
objects = Array.new(SecureRandom.random_number(9) + 2) do
|
127
|
+
object = factory.string(SecureRandom.hex(4))
|
128
|
+
object.set(SecureRandom.hex(4))
|
129
|
+
object
|
130
|
+
end
|
131
|
+
|
132
|
+
return factory, objects.map(&:key)
|
133
|
+
end
|
134
|
+
private :populated_factory
|
135
|
+
end
|
136
|
+
end
|