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.
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