redstruct 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -11
  3. data/Rakefile +5 -5
  4. data/lib/redstruct/all.rb +14 -0
  5. data/lib/redstruct/configuration.rb +9 -6
  6. data/lib/redstruct/connection_proxy.rb +123 -0
  7. data/lib/redstruct/counter.rb +96 -0
  8. data/lib/redstruct/error.rb +2 -0
  9. data/lib/redstruct/factory/object.rb +31 -0
  10. data/lib/redstruct/factory.rb +94 -55
  11. data/lib/redstruct/hash.rb +123 -0
  12. data/lib/redstruct/list.rb +315 -0
  13. data/lib/redstruct/lock.rb +183 -0
  14. data/lib/redstruct/script.rb +104 -0
  15. data/lib/redstruct/set.rb +155 -0
  16. data/lib/redstruct/sorted_set/slice.rb +124 -0
  17. data/lib/redstruct/sorted_set.rb +153 -0
  18. data/lib/redstruct/string.rb +66 -0
  19. data/lib/redstruct/struct.rb +87 -0
  20. data/lib/redstruct/utils/coercion.rb +14 -8
  21. data/lib/redstruct/utils/inspectable.rb +8 -4
  22. data/lib/redstruct/utils/iterable.rb +52 -0
  23. data/lib/redstruct/utils/scriptable.rb +32 -6
  24. data/lib/redstruct/version.rb +4 -1
  25. data/lib/redstruct.rb +17 -51
  26. data/lib/yard/defscript_handler.rb +5 -3
  27. data/test/redstruct/configuration_test.rb +13 -0
  28. data/test/redstruct/connection_proxy_test.rb +85 -0
  29. data/test/redstruct/counter_test.rb +108 -0
  30. data/test/redstruct/factory/object_test.rb +21 -0
  31. data/test/redstruct/factory_test.rb +136 -0
  32. data/test/redstruct/hash_test.rb +138 -0
  33. data/test/redstruct/list_test.rb +244 -0
  34. data/test/redstruct/lock_test.rb +108 -0
  35. data/test/redstruct/script_test.rb +53 -0
  36. data/test/redstruct/set_test.rb +219 -0
  37. data/test/redstruct/sorted_set/slice_test.rb +10 -0
  38. data/test/redstruct/sorted_set_test.rb +219 -0
  39. data/test/redstruct/string_test.rb +8 -0
  40. data/test/redstruct/struct_test.rb +61 -0
  41. data/test/redstruct/utils/coercion_test.rb +33 -0
  42. data/test/redstruct/utils/inspectable_test.rb +31 -0
  43. data/test/redstruct/utils/iterable_test.rb +94 -0
  44. data/test/redstruct/utils/scriptable_test.rb +67 -0
  45. data/test/redstruct_test.rb +14 -0
  46. data/test/test_helper.rb +77 -1
  47. metadata +58 -26
  48. data/lib/redstruct/connection.rb +0 -47
  49. data/lib/redstruct/factory/creation.rb +0 -95
  50. data/lib/redstruct/factory/deserialization.rb +0 -7
  51. data/lib/redstruct/hls/lock.rb +0 -175
  52. data/lib/redstruct/hls/queue.rb +0 -29
  53. data/lib/redstruct/hls.rb +0 -2
  54. data/lib/redstruct/types/base.rb +0 -36
  55. data/lib/redstruct/types/counter.rb +0 -65
  56. data/lib/redstruct/types/hash.rb +0 -72
  57. data/lib/redstruct/types/list.rb +0 -76
  58. data/lib/redstruct/types/script.rb +0 -56
  59. data/lib/redstruct/types/set.rb +0 -96
  60. data/lib/redstruct/types/sorted_set.rb +0 -129
  61. data/lib/redstruct/types/string.rb +0 -64
  62. data/lib/redstruct/types/struct.rb +0 -58
  63. data/lib/releaser/logger.rb +0 -15
  64. data/lib/releaser/repository.rb +0 -32
  65. data/lib/tasks/release.rake +0 -49
  66. 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
- def defscript(id, source)
10
- constant = "SCRIPT_SOURCE_#{id.upcase}"
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
- #{constant} = { id: '#{id}'.freeze, source: %(#{source}).freeze }.freeze
13
- def #{id}(keys: [], argv: [])
14
- return @factory.script(#{constant}[:id], #{constant}[:source]).eval(keys: keys, argv: argv)
15
- end
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
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Redstruct
2
- VERSION = '0.1.7'.freeze
4
+ # Current version
5
+ VERSION = '0.2.0'
3
6
  end
data/lib/redstruct.rb CHANGED
@@ -1,65 +1,31 @@
1
- # Dependencies
2
- require 'redis'
3
- require 'connection_pool'
1
+ # frozen_string_literal: true
4
2
 
5
- # Utility
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
- # Base data types
23
- require 'redstruct/types/struct'
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
- def factories
39
- return @factories ||= {}
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 [](key)
43
- factory = factories[key]
44
- factory = make(name: key) if factory.nil?
45
-
46
- return factory
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
- module Redstruct
2
- class DefscriptHandler < YARD::Handlers::Ruby::Base
3
- GROUP_NAME = 'Lua Scripts'.freeze
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