redstruct 0.1.7 → 0.2.0
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.
- 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
|