mock_redis_lua_extension 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9c9ac03c2745ae7cf81444d9b5d92ec76be1685ecd99806037152d18e8bf2d66
4
+ data.tar.gz: b2d472940117687fded0d9cdf63b882390a0a5cdab767912fe82db371f110499
5
+ SHA512:
6
+ metadata.gz: cd745df3ac161d1f1fce18febcb482ac499032bc83c6d74195b5d695422b5549b24747b047cd788a521abe98245a4726c644a81ec33049199f994949cc292575
7
+ data.tar.gz: f2a7a673f36e0bd347cb57ef66a95fbbacfd5eba68e5e606401502375c7e8111f320de11cdeba360e32de91fed4737aaeb7af1b619d266dd00d31f2d5c1dacd0
data/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'pry'
7
+ gem 'rake', '~> 11.0'
8
+ gem 'rspec', '~> 3.0'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mock_redis_lua_extension (0.2.0)
5
+ mock_redis
6
+ rufus-lua
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ coderay (1.1.2)
12
+ diff-lcs (1.3)
13
+ ffi (1.9.25)
14
+ method_source (0.9.0)
15
+ mock_redis (0.19.0)
16
+ pry (0.11.3)
17
+ coderay (~> 1.1.0)
18
+ method_source (~> 0.9.0)
19
+ rake (11.3.0)
20
+ rspec (3.7.0)
21
+ rspec-core (~> 3.7.0)
22
+ rspec-expectations (~> 3.7.0)
23
+ rspec-mocks (~> 3.7.0)
24
+ rspec-core (3.7.1)
25
+ rspec-support (~> 3.7.0)
26
+ rspec-expectations (3.7.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.7.0)
29
+ rspec-mocks (3.7.0)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.7.0)
32
+ rspec-support (3.7.1)
33
+ rufus-lua (1.1.5)
34
+ ffi (~> 1.9)
35
+
36
+ PLATFORMS
37
+ ruby
38
+
39
+ DEPENDENCIES
40
+ mock_redis_lua_extension!
41
+ pry
42
+ rake (~> 11.0)
43
+ rspec (~> 3.0)
44
+
45
+ BUNDLED WITH
46
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Invoca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # mock_redis_lua_extension
2
+ Extension to mock_redis enabling lua execution via rufus-lua
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ begin
11
+ require 'rspec/core/rake_task'
12
+ RSpec::Core::RakeTask.new(:spec)
13
+
14
+ task :default => :spec
15
+ rescue LoadError
16
+ puts 'No Rspec available'
17
+ end
@@ -0,0 +1,273 @@
1
+ begin
2
+ require 'rufus-lua'
3
+ RUFUS_LUA_LOADED = true
4
+ rescue StandardError => ex
5
+ RUFUS_LUA_LOADED = false
6
+ STDERR.puts "Failed to load rufus-lua: Exception was #{ex.inspect}"
7
+ end
8
+
9
+ require 'json'
10
+ require 'digest'
11
+
12
+ module MockRedisLuaExtension
13
+ class InvalidCommand < StandardError; end
14
+ class InvalidDataType < StandardError; end
15
+
16
+ LIMIT_CMDS = [
17
+ 'zrangebyscore',
18
+ 'zrangebylex',
19
+ 'zrevrangebyscore',
20
+ 'zrevrangebylex'
21
+ ].freeze
22
+
23
+ WITHSCORES_CMDS = [
24
+ 'zrange',
25
+ 'zrangebyscore',
26
+ 'zrevrangebyscore'
27
+ ].freeze
28
+
29
+ def self.wrap(instance)
30
+ if !instance.respond_to?(:mock_redis_lua_extension_enabled) && is_a_mock?(instance)
31
+ class << instance
32
+ if RUFUS_LUA_LOADED
33
+ prepend(MockRedisLuaExtension)
34
+ end
35
+ end
36
+ elsif !is_a_mock?(instance)
37
+ raise ArgumentError, 'Can only wrap MockRedis instances'
38
+ end
39
+ instance
40
+ end
41
+
42
+ def self.is_a_mock?(instance)
43
+ instance.class.ancestors.any? { |a| a.to_s == 'MockRedis' }
44
+ end
45
+
46
+ def mock_redis_lua_extension_enabled
47
+ RUFUS_LUA_LOADED
48
+ end
49
+
50
+ def script(subcmd, *args)
51
+ case subcmd.downcase.to_sym
52
+ when :load
53
+ args.count == 1 or raise ArgumentError, "Invalid args: #{args.inspect}"
54
+ script = args.first
55
+ Digest::SHA256.hexdigest(script).tap do |sha|
56
+ script_catalog[sha] = script
57
+ end
58
+ when :flush
59
+ @script_catalog = {}
60
+ true
61
+ when :exists
62
+ args = args.first
63
+ if args.is_a?(Array)
64
+ args.map { |sha| script_catalog.include?(sha) }
65
+ else
66
+ script_catalog.include?(args)
67
+ end
68
+ else
69
+ raise ArgumentError, "Invalid script command: #{subcmd}"
70
+ end
71
+ end
72
+
73
+ def evalsha(sha, keys=nil, argv=nil, **args)
74
+ if script(:exists, sha)
75
+ eval(script_catalog[sha], keys, argv, **args)
76
+ else
77
+ raise ArgumentError, "NOSCRIPT No matching script. Please use EVAL."
78
+ end
79
+ end
80
+
81
+ def eval(script, keys=nil, argv=nil, **args)
82
+ lua_state = Rufus::Lua::State.new
83
+ setup_keys_and_argv(lua_state, keys, argv, args)
84
+
85
+ lua_state.function 'redis.call' do |cmd, *args|
86
+ lua_bound_redis_call(cmd, *args)
87
+ end
88
+
89
+ lua_state.function 'redis.breakpoint' do
90
+ # Use redis commands such as `self.hgetall("key")` to debug
91
+ binding.pry
92
+ end
93
+
94
+ lua_state.function 'redis.debug' do |*args|
95
+ parsed_args = args.map { |arg| arg.is_a?(Rufus::Lua::Table) ? arg.to_ruby : arg }
96
+ puts parsed_args.map(&:to_s).join(", ")
97
+ end
98
+
99
+ lua_state.function 'cjson.decode' do |arg|
100
+ lua_bound_cjson_decode(arg)
101
+ end
102
+
103
+ lua_state.function 'cjson.encode' do |arg|
104
+ lua_bound_cjson_encode(arg)
105
+ end
106
+ marshal_lua_return_to_ruby(lua_state.eval(script))
107
+ end
108
+
109
+ private
110
+
111
+ def script_catalog
112
+ @script_catalog ||= {}
113
+ end
114
+
115
+ def lua_bound_redis_call(cmd, *args)
116
+ cmd = cmd.downcase
117
+ if valid_lua_bound_cmds.include?(cmd.to_sym)
118
+ redis_call_from_lua(cmd, *args)
119
+ else
120
+ raise InvalidCommand, "Invalid command (cmd: #{cmd}, args: #{args.inspect})"
121
+ end
122
+ rescue InvalidDataType => ex
123
+ raise InvalidCommand, "Invalid command (cmd: #{cmd}, args: #{args.inspect}) caused by #{ex.class}(#{ex.message})"
124
+ end
125
+
126
+ def lua_bound_cjson_decode(arg)
127
+ JSON.parse(arg)
128
+ end
129
+
130
+ def lua_bound_cjson_encode(arg)
131
+ case arg
132
+ when String, Float, Integer, NilClass
133
+ arg.to_json
134
+ when Rufus::Lua::Table
135
+ table_to_array_or_hash(arg).to_json
136
+ else
137
+ raise InvalidDataType, "Unexpected data type for cjson.encode: #{arg.inspect}"
138
+ end
139
+ end
140
+
141
+ def setup_keys_and_argv(lua_state, keys, argv, args)
142
+ keys = [] unless keys
143
+ keys = args[:keys] if args[:keys]
144
+
145
+ argv = [] unless argv
146
+ argv = args[:argv] if args[:argv]
147
+
148
+ lua_state['KEYS'] = keys.map { |k| k.to_s }
149
+ lua_state['ARGV'] = argv.map { |a| a.to_s }
150
+ end
151
+
152
+ def redis_call_from_lua(cmd, *args)
153
+ redis_args = marshal_lua_args_to_redis(cmd, args)
154
+ redis_result = self.send(cmd, *redis_args)
155
+ marshal_redis_result_to_lua(redis_result, flatten_array: WITHSCORES_CMDS.include?(cmd.downcase))
156
+ end
157
+
158
+ def marshal_lua_args_to_redis(cmd, args)
159
+ options, args = parse_options(cmd, args)
160
+ converted_args = args.map do |arg|
161
+ case arg
162
+ when Float, Integer
163
+ arg.to_s
164
+ when String
165
+ arg
166
+ else
167
+ raise InvalidDataType, "Lua redis() command arguments must be strings or numbers (was: #{args.inspect})"
168
+ end
169
+ end
170
+ if options.any? { |_, v| v }
171
+ converted_args + [options]
172
+ else
173
+ converted_args
174
+ end
175
+ end
176
+
177
+ def parse_options(cmd, args)
178
+ limit, args = if args[-3].to_s.downcase == 'limit' && LIMIT_CMDS.include?(cmd)
179
+ [args[-2..-1], args[0...-3]]
180
+ else
181
+ [nil, args]
182
+ end
183
+
184
+ withscores, args = if args[-1].to_s.downcase == 'withscores' && WITHSCORES_CMDS.include?(cmd)
185
+ [true, args[0...-1]]
186
+ else
187
+ [nil, args]
188
+ end
189
+
190
+ return { limit: limit, with_scores: withscores }, args
191
+ end
192
+
193
+ def marshal_redis_result_to_lua(result, **options)
194
+ case result
195
+ when nil
196
+ false
197
+ when true
198
+ 1
199
+ when false
200
+ 0
201
+ when Array
202
+ options[:flatten_array] ? result.flatten : result
203
+ when Integer, String
204
+ result
205
+ when Float
206
+ result.to_s
207
+ else
208
+ raise InvalidDataType, "Unsupported type returned from redis (was: #{result.inspect})"
209
+ end
210
+ end
211
+
212
+ def marshal_lua_return_to_ruby(arg)
213
+ case arg
214
+ when false
215
+ nil
216
+ when true
217
+ 1
218
+ when Float, Integer
219
+ arg.to_i
220
+ when String, Array, nil
221
+ arg
222
+ when Rufus::Lua::Table
223
+ table_to_array_or_status(arg)
224
+ else
225
+ raise InvalidDataType, "Unsupported type returned from script (was: #{arg.inspect})"
226
+ end
227
+ end
228
+
229
+ def table_to_array_or_status(table)
230
+ (1..table.keys.length).map do |i|
231
+ marshal_lua_return_to_ruby(table[i.to_f])
232
+ end.compact
233
+ end
234
+
235
+ def table_to_array_or_hash(table)
236
+ if table.keys.all? { |k| k.is_a?(Numeric) && k >=0 }
237
+ table.to_a
238
+ else
239
+ table.to_h
240
+ end
241
+ end
242
+
243
+ def valid_lua_bound_cmds
244
+ @valid_lua_bound_cmds ||= Hash[[
245
+ #Hash commands
246
+ :hdel, :hexists, :hget, :hgetall, :hincrby, :hincrbyfloat, :hkeys, :hlen,
247
+ :hmget, :hmset, :hset, :hsetnx, :hstrlen, :hvals, :hscan,
248
+
249
+ #Key commands
250
+ :del, :dump, :exists, :expire, :expireat, :keys, :persist, :pexpire, :pexpireat,
251
+ :pttl, :randomkey, :rename, :renamenx, :sort, :touch, :ttl, :type, :unlink,
252
+
253
+ #List commands
254
+ :blpop, :brpop, :brpoplpush, :lindex, :linsert, :llen, :lpop, :lpush, :lpushx,
255
+ :lrange, :lrem, :lset, :ltrim, :rpop, :rpoplpush, :rpush, :rpushx,
256
+
257
+ #Set commands
258
+ :sadd, :scard, :sdiff, :sdiffstore, :sinter, :sinterstore, :sismember, :smembers,
259
+ :smove, :spop, :srandmember, :srem, :sunion, :sunionstore, :sscan,
260
+
261
+ #SortedSet commands
262
+ :zadd, :zcard, :zcount, :zincrby, :zinterstore, :zlexcount, :zrange, :zrangebylex,
263
+ :zrevrangebylex, :zrangebyscore, :zrank, :zrem, :zremrangebylex, :zremrangebyrank,
264
+ :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zscore, :zunionstore,
265
+ :zscan,
266
+
267
+ #String commands
268
+ :append, :bitcount, :bitfield, :bitop, :bitpos, :decr, :decrby, :get, :getbit,
269
+ :getrange, :getset, :incr, :incrby, :incrbyfloat, :mget, :mset, :msetnx, :psetex,
270
+ :set, :setbit, :setex, :setnx, :setrange, :strlen
271
+ ].map {|cmd| [cmd, true] }]
272
+ end
273
+ end
@@ -0,0 +1,3 @@
1
+ module MockRedisLuaExtension
2
+ VERSION = "0.2.0".freeze
3
+ end
@@ -0,0 +1,20 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+
3
+ require "mock_redis_lua_extension/version"
4
+
5
+ # Describe your gem and declare its dependencies:
6
+ Gem::Specification.new do |s|
7
+ s.name = "mock_redis_lua_extension"
8
+ s.version = MockRedisLuaExtension::VERSION
9
+ s.authors = ["Chad Simmons"]
10
+ s.email = ["csimmons@invoca.com"]
11
+ s.homepage = "https://github.com/Invoca/mock_redis_lua_extension"
12
+ s.summary = "Extension to mock_redis enabling lua execution via rufus-lua"
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- spec/*`.split("\n")
16
+ s.require_paths = ['lib']
17
+
18
+ s.add_dependency 'mock_redis'
19
+ s.add_dependency 'rufus-lua'
20
+ end
@@ -0,0 +1,324 @@
1
+ require 'spec_helper'
2
+ require 'mock_redis_lua_extension'
3
+ require 'mock_redis'
4
+
5
+ require 'pry'
6
+
7
+ RSpec.describe MockRedisLuaExtension, '::' do
8
+
9
+ let(:redis) { MockRedisLuaExtension.wrap(MockRedis.new) }
10
+
11
+ context 'extending a MockRedis instance' do
12
+ it 'should add a method indicating that the MockRedis has been extended' do
13
+ expect(redis.respond_to?(:mock_redis_lua_extension_enabled)).to eq(true)
14
+ end
15
+
16
+ it 'should raise an ArgumentError when attempting to wrap an object that is not a MockRedis' do
17
+ expect { MockRedisLuaExtension.wrap(Object.new) }.to raise_error(ArgumentError,
18
+ 'Can only wrap MockRedis instances')
19
+ end
20
+
21
+ it 'supports eval with redis bound to self' do
22
+ redis.hset('myhash', 'field', 5)
23
+ lua_script = %q|
24
+ redis.call('hincrby', KEYS[1], ARGV[1], ARGV[2])
25
+ |.strip
26
+ redis.eval(lua_script, keys: ['myhash'], argv: ['field', 2])
27
+ value = redis.hget('myhash', 'field')
28
+ expect(value).to eq('7')
29
+ end
30
+
31
+ it 'supports the script command to load scripts for use with evalsha' do
32
+ sha = redis.script(:load, 'return "EXECUTED"')
33
+ expect(redis.evalsha(sha)).to eq('EXECUTED')
34
+ end
35
+
36
+ it 'supports script exists' do
37
+ sha = redis.script(:load, 'return "EXECUTED"')
38
+ expect(redis.script(:exists, sha)).to eq(true)
39
+
40
+ expect(redis.script(:exists, '1114444')).to eq(false)
41
+ end
42
+
43
+ it 'supports script exists with multiple shas' do
44
+ sha1 = redis.script(:load, 'return "EXECUTED"')
45
+ sha2 = redis.script(:load, 'return "DIFFERENT"')
46
+ expect(redis.script(:exists, [sha1, sha2, 'invalidsha'])).to eq([true, true, false])
47
+ end
48
+
49
+ it 'supports evalsha' do
50
+ sha = redis.script(:load, 'return { KEYS[1], ARGV[1] }')
51
+ expect(redis.evalsha(sha, ['key1', 'key2'], ['arg1', 'arg2'])).to eq(['key1', 'arg1'])
52
+ end
53
+
54
+ context 'eval arguments' do
55
+ it 'passes keys as KEYS table' do
56
+ result = redis.eval('return KEYS[1]', keys: ['first_key', 'second_key'])
57
+ expect(result).to eq('first_key')
58
+
59
+ result = redis.eval('return KEYS[2]', ['first_key', 'second_key'])
60
+ expect(result).to eq('second_key')
61
+ end
62
+
63
+ it 'passes argv as ARGV table' do
64
+ result = redis.eval('return ARGV[1]', argv: ['first', 'second'])
65
+ expect(result).to eq('first')
66
+
67
+ result = redis.eval('return ARGV[2]', [], ['first', 'second'])
68
+ expect(result).to eq('second')
69
+ end
70
+
71
+ it 'should convert keys and argv to lists of strings' do
72
+ result = redis.eval('return ARGV[2]', argv: [nil, 2.4])
73
+ expect(result).to eq('2.4')
74
+
75
+ result = redis.eval('return KEYS[1]', keys: [:stuff])
76
+ expect(result).to eq('stuff')
77
+ end
78
+ end
79
+
80
+ context 'marshalling lua args to redis.call' do
81
+ it 'should convert lua numbers to strings' do
82
+ redis.eval(%q| redis.call('set', 'foo', 1.5) |)
83
+ expect(redis.get('foo')).to eq('1.5')
84
+ end
85
+
86
+ it 'should raise an error if args are not strings or numbers' do
87
+ lua_script = %q|
88
+ redis.call('set', 'foo', {'a', 'b', 'c'})
89
+ |.strip
90
+ expect { redis.eval(lua_script) }.to raise_error(MockRedisLuaExtension::InvalidCommand) do |ex|
91
+ expect(ex.message).to match('caused by MockRedisLuaExtension::InvalidDataType')
92
+ end
93
+ end
94
+
95
+ context 'hash options' do
96
+ before do
97
+ redis.zadd('foo', 1, 'washington')
98
+ redis.zadd('foo', 2, 'jefferson')
99
+ redis.zadd('foo', 3, 'adams')
100
+ redis.zadd('foo', 4, 'madison')
101
+ end
102
+
103
+ it 'should convert limits into a hash option' do
104
+ lua_script = %q|
105
+ return redis.call('ZRANGEBYSCORE', 'foo', 2, 4, 'LIMIT', 0, 2)
106
+ |.strip
107
+ expect(redis.eval(lua_script)).to eq(['jefferson', 'adams'])
108
+ end
109
+
110
+ it 'should convert withscores into a hash option' do
111
+ lua_script = %q|
112
+ return redis.call('ZRANGEBYSCORE', 'foo', 2, 3, 'WITHSCORES')
113
+ |.strip
114
+ expected_result = ['jefferson', 2.0, 'adams', 3.0]
115
+ expect(redis.eval(lua_script)).to eq(expected_result)
116
+ end
117
+
118
+ it 'should support both hash options' do
119
+ lua_script = %q|
120
+ return redis.call('ZRANGEBYSCORE', 'foo', 2, 4, 'WITHSCORES', 'LIMIT', 1, 2)
121
+ |.strip
122
+ expected_result = ['adams', 3.0, 'madison', 4.0]
123
+ expect(redis.eval(lua_script)).to eq(expected_result)
124
+ end
125
+ end
126
+ end
127
+
128
+ context 'marshalling lua return values to ruby' do
129
+ it 'should convert true to 1' do
130
+ expect(redis.eval(%q| return true |)).to eq(1)
131
+ end
132
+
133
+ it 'should convert false to nil' do
134
+ expect(redis.eval(%q| return false |)).to eq(nil)
135
+ end
136
+
137
+ it 'should convert numbers to integers' do
138
+ expect(redis.eval(%q| return 2.3 |)).to eq(2)
139
+ end
140
+
141
+ it 'should leave strings as is' do
142
+ expect(redis.eval(%q| return 'a simple string'|)).to eq('a simple string')
143
+ end
144
+
145
+ it 'should convert tables to arrays (ignoring keys)' do
146
+ expect(redis.eval(%q| return {foo='bar', 'a', 'b', 'c'} |)).to eq(['a', 'b', 'c'])
147
+ end
148
+
149
+ it 'should return redis success responses as "OK"' do
150
+ lua_script = %q|
151
+ return redis.call('set', 'foo', 'bar')
152
+ |.strip
153
+ expect(redis.eval(lua_script)).to eq('OK')
154
+ end
155
+
156
+ it 'should correctly marshall nested tables' do
157
+ expect(redis.eval(%q| return {foo='bar', 'a', 1.4, {'b', 2.7}} |)).to eq(['a', 1, ['b', 2]])
158
+ end
159
+ end
160
+
161
+ context 'cjson implementation' do
162
+ it 'should decode json strings' do
163
+ lua_script = %q|
164
+ local result = cjson.decode('{"foo":"bar","baz":4,"nil_value":null}')
165
+ return { result.foo, result.baz, result.nil_value }
166
+ |.strip
167
+ expect(redis.eval(lua_script)).to eq(['bar', 4])
168
+ end
169
+
170
+ it 'should encode hash-style tables' do
171
+ lua_script = %q|
172
+ return cjson.encode({ foo='bar', baz=4, null_value=nil })
173
+ |.strip
174
+ expect(redis.eval(lua_script)).to eq('{"baz":4.0,"foo":"bar"}')
175
+ end
176
+
177
+ it 'should encode array-style tables' do
178
+ lua_script = %q|
179
+ return cjson.encode({ 'bar', 4, nil })
180
+ |.strip
181
+ expect(redis.eval(lua_script)).to eq('["bar",4.0]')
182
+ end
183
+
184
+ it 'should encode strings' do
185
+ lua_script = %q|
186
+ return cjson.encode('in_service')
187
+ |.strip
188
+ expect(redis.eval(lua_script)).to eq('"in_service"')
189
+ end
190
+
191
+ it 'should encode nil' do
192
+ lua_script = %q|
193
+ return cjson.encode(nil)
194
+ |.strip
195
+ expect(redis.eval(lua_script)).to eq('null')
196
+ end
197
+
198
+ it 'should encode numbers' do
199
+ lua_script = %q|
200
+ return cjson.encode(4)
201
+ |.strip
202
+ expect(redis.eval(lua_script)).to eq('4.0')
203
+ end
204
+
205
+ it 'should encode floats' do
206
+ lua_script = %q|
207
+ return cjson.encode(4.2)
208
+ |.strip
209
+ expect(redis.eval(lua_script)).to eq('4.2')
210
+ end
211
+ end
212
+
213
+ context 'marshalling redis.call return values to lua' do
214
+ it 'should convert nil to false' do
215
+ lua_script = %q|
216
+ local value = redis.call('get', 'not_defined')
217
+ if value == nil then
218
+ redis.call('set', 'value', 'was nil')
219
+ elseif value == false then
220
+ redis.call('set', 'value', 'was false')
221
+ end
222
+ |.strip
223
+ redis.eval(lua_script)
224
+ expect(redis.get('not_defined')).to eq(nil)
225
+ expect(redis.get('value')).to eq('was false')
226
+ end
227
+
228
+ it 'should marshall arrays into tables' do
229
+ redis.zadd('myset', 1, 'one')
230
+ redis.zadd('myset', 2, 'two')
231
+ redis.zadd('myset', 3, 'three')
232
+ lua_script = %q|
233
+ local result = redis.call('zrangebyscore', 'myset', 2, 3)
234
+ return { result[1], result[2] }
235
+ |.strip
236
+ expect(redis.eval(lua_script)).to eq(['two', 'three'])
237
+ end
238
+
239
+ it 'should leave strings and numbers as is' do
240
+ redis.lpush('string_value', 'value')
241
+ lua_script = %q|
242
+ local value = redis.call('lindex', 'string_value', 0)
243
+ if value == 'value' then
244
+ redis.call('set', 'string_result', 'was unchanged')
245
+ else
246
+ redis.call('set', 'string_result', 'was changed')
247
+ end
248
+
249
+ value = redis.call('llen', 'string_value')
250
+ if value == 1 then
251
+ redis.call('set', 'number_result', 'was unchanged')
252
+ else
253
+ redis.call('set', 'number_result', 'was changed')
254
+ end
255
+ |.strip
256
+ redis.eval(lua_script)
257
+ expect(redis.get('string_result')).to eq('was unchanged')
258
+ expect(redis.get('number_result')).to eq('was unchanged')
259
+ end
260
+
261
+ it 'should return scores as strings' do
262
+ redis.zadd('myset', 1, 'one')
263
+ redis.zadd('myset', 2, 'two')
264
+ redis.zadd('myset', 3, 'three')
265
+ lua_script = %q|
266
+ local result = redis.call('zscore', 'myset', 'two')
267
+ return result
268
+ |.strip
269
+
270
+ expect(redis.eval(lua_script)).to eq('2.0')
271
+ end
272
+
273
+
274
+ it 'should convert true to 1 and false to 0 when returning from redis' do
275
+ expect(redis.hset('myhash', 'existing_key', 'first')).to eq(true)
276
+ expect(redis.hset('myhash', 'existing_key', 'second')).to eq(false)
277
+ lua_script = %q|
278
+ local value = redis.call('hset', 'myhash', 'new_key', 'value')
279
+ if value == 1 then
280
+ redis.call('set', 'true_value', 'was 1')
281
+ else
282
+ redis.call('set', 'true_value', 'was not 1')
283
+ end
284
+
285
+ value = redis.call('hset', 'myhash', 'existing_key', 'third')
286
+ if value == 0 then
287
+ redis.call('set', 'false_value', 'was 0')
288
+ else
289
+ redis.call('set', 'false_value', 'was not 0')
290
+ end
291
+ |.strip
292
+ redis.eval(lua_script)
293
+ expect(redis.get('true_value')).to eq('was 1')
294
+ expect(redis.get('false_value')).to eq('was 0')
295
+ end
296
+ end
297
+
298
+ context "redis.breakpoint" do
299
+ it 'calls binding.pry when redis.breakpoint() is called' do
300
+ expect_any_instance_of(Binding).to receive(:pry)
301
+ redis.eval("redis.breakpoint()")
302
+ end
303
+
304
+ it 'puts parsed args when redis.debug() is called' do
305
+ expect_any_instance_of(MockRedis).to receive(:puts).with("hello, hi, 1.0, [5.0, 10.0], {\"monkey\"=>\"banana\", \"number\"=>200.0}, goodbye")
306
+ lua_script = %q|
307
+ local var1 = "hi"
308
+ local var2 = 1.0
309
+
310
+ local my_array = {}
311
+ table.insert(my_array, 5)
312
+ table.insert(my_array, 10)
313
+
314
+ local my_hash = {}
315
+ my_hash["monkey"] = "banana"
316
+ my_hash["number"] = 200
317
+
318
+ redis.debug("hello", var1, var2, my_array, my_hash, "goodbye")
319
+ |.strip
320
+ redis.eval(lua_script)
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,88 @@
1
+ require 'pry'
2
+
3
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
4
+ RSpec.configure do |config|
5
+ # rspec-expectations config goes here. You can use an alternate
6
+ # assertion/expectation library such as wrong or the stdlib/minitest
7
+ # assertions if you prefer.
8
+ config.expect_with :rspec do |expectations|
9
+ # This option will default to `true` in RSpec 4. It makes the `description`
10
+ # and `failure_message` of custom matchers include text for helper methods
11
+ # defined using `chain`, e.g.:
12
+ # be_bigger_than(2).and_smaller_than(4).description
13
+ # # => "be bigger than 2 and smaller than 4"
14
+ # ...rather than:
15
+ # # => "be bigger than 2"
16
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
17
+ end
18
+
19
+ # rspec-mocks config goes here. You can use an alternate test double
20
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
21
+ config.mock_with :rspec do |mocks|
22
+ # Prevents you from mocking or stubbing a method that does not exist on
23
+ # a real object. This is generally recommended, and will default to
24
+ # `true` in RSpec 4.
25
+ mocks.verify_partial_doubles = true
26
+ end
27
+
28
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
29
+ # have no way to turn it off -- the option exists only for backwards
30
+ # compatibility in RSpec 3). It causes shared context metadata to be
31
+ # inherited by the metadata hash of host groups and examples, rather than
32
+ # triggering implicit auto-inclusion in groups with matching metadata.
33
+ config.shared_context_metadata_behavior = :apply_to_host_groups
34
+
35
+ # The settings below are suggested to provide a good initial experience
36
+ # with RSpec, but feel free to customize to your heart's content.
37
+ =begin
38
+ # This allows you to limit a spec run to individual examples or groups
39
+ # you care about by tagging them with `:focus` metadata. When nothing
40
+ # is tagged with `:focus`, all examples get run. RSpec also provides
41
+ # aliases for `it`, `describe`, and `context` that include `:focus`
42
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
43
+ config.filter_run_when_matching :focus
44
+
45
+ # Allows RSpec to persist some state between runs in order to support
46
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
47
+ # you configure your source control system to ignore this file.
48
+ config.example_status_persistence_file_path = "spec/examples.txt"
49
+
50
+ # Limits the available syntax to the non-monkey patched syntax that is
51
+ # recommended. For more details, see:
52
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
53
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
54
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
55
+ config.disable_monkey_patching!
56
+
57
+ # This setting enables warnings. It's recommended, but in some cases may
58
+ # be too noisy due to issues in dependencies.
59
+ config.warnings = true
60
+
61
+ # Many RSpec users commonly either run the entire suite or an individual
62
+ # file, and it's useful to allow more verbose output when running an
63
+ # individual spec file.
64
+ if config.files_to_run.one?
65
+ # Use the documentation formatter for detailed output,
66
+ # unless a formatter has already been configured
67
+ # (e.g. via a command-line flag).
68
+ config.default_formatter = "doc"
69
+ end
70
+
71
+ # Print the 10 slowest examples and example groups at the
72
+ # end of the spec run, to help surface which specs are running
73
+ # particularly slow.
74
+ config.profile_examples = 10
75
+
76
+ # Run specs in random order to surface order dependencies. If you find an
77
+ # order dependency and want to debug it, you can fix the order by providing
78
+ # the seed, which is printed after each run.
79
+ # --seed 1234
80
+ config.order = :random
81
+
82
+ # Seed global randomization in this process using the `--seed` CLI option.
83
+ # Setting this allows you to use `--seed` to deterministically reproduce
84
+ # test failures related to randomization by passing the same `--seed` value
85
+ # as the one that triggered the failure.
86
+ Kernel.srand config.seed
87
+ =end
88
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mock_redis_lua_extension
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Chad Simmons
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mock_redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rufus-lua
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description:
42
+ email:
43
+ - csimmons@invoca.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".rspec"
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - lib/mock_redis_lua_extension.rb
56
+ - lib/mock_redis_lua_extension/version.rb
57
+ - mock_redis_lua_extension.gemspec
58
+ - spec/mock_redis_lua_extension_spec.rb
59
+ - spec/spec_helper.rb
60
+ homepage: https://github.com/Invoca/mock_redis_lua_extension
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.7.7
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Extension to mock_redis enabling lua execution via rufus-lua
83
+ test_files:
84
+ - spec/mock_redis_lua_extension_spec.rb
85
+ - spec/spec_helper.rb