diff --git a/Gemfile b/Gemfile index 16f3338..430f85a 100644 --- a/Gemfile +++ b/Gemfile @@ -9,5 +9,6 @@ gem 'overcommit', '0.62.0' # Pin tool versions (which are executed by Overcommit) for CI builds gem 'rubocop', '1.44.1' +gem 'base64', '~> 0.2.0' gem 'simplecov', '~> 0.22.0' gem 'simplecov-lcov', '~> 0.8.0' diff --git a/lib/mock_redis/list_methods.rb b/lib/mock_redis/list_methods.rb index 69c338b..7f7f993 100644 --- a/lib/mock_redis/list_methods.rb +++ b/lib/mock_redis/list_methods.rb @@ -90,6 +90,32 @@ def llen(key) with_list_at(key, &:length) end + def lmpop(*keys, **options) + keys.each do |key| + assert_listy(key) + end + + modifier = options.is_a?(Hash) && options[:modifier]&.to_s&.downcase || 'left' + count = (options.is_a?(Hash) && options[:count]) || 1 + + unless %w[left right].include?(modifier) + raise Redis::CommandError, 'ERR syntax error' + end + + keys.each do |key| + record_count = llen(key) + next if record_count.zero? + + values = [count, record_count].min.times.map do + modifier == 'left' ? with_list_at(key, &:shift) : with_list_at(key, &:pop) + end + + return [key, values] + end + + nil + end + def lmove(source, destination, wherefrom, whereto) assert_listy(source) assert_listy(destination) diff --git a/spec/commands/lmpop_spec.rb b/spec/commands/lmpop_spec.rb new file mode 100644 index 0000000..d7f753e --- /dev/null +++ b/spec/commands/lmpop_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +RSpec.describe '#lmpop(*keys)', redis: 7.0 do + before do + @list1 = 'mock-redis-test:lmpop-list' + @list2 = 'mock-redis-test:lmpop-list2' + + @redises.lpush(@list1, 'c') + @redises.lpush(@list1, 'b') + @redises.lpush(@list1, 'a') + + @redises.lpush(@list2, 'z') + @redises.lpush(@list2, 'y') + @redises.lpush(@list2, 'x') + end + + it 'returns and removes the first element of the first non-empty list' do + expect(@redises.lmpop('empty', @list1, @list2)).to eq([@list1, ['a']]) + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[b c]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'returns and removes the first element of the first non-empty list when modifier is LEFT' do + expect(@redises.lmpop('empty', @list1, @list2, modifier: 'LEFT')).to eq([@list1, ['a']]) + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[b c]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'returns and removes the last element of the first non-empty list when modifier is RIGHT' do + expect(@redises.lmpop('empty', @list1, @list2, modifier: 'RIGHT')).to eq([@list1, ['c']]) + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[a b]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'returns and removes multiple elements from the front when count is given' do + expect(@redises.lmpop('empty', @list1, @list2, count: 2)).to eq([@list1, %w[a b]]) + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[c]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'returns and removes multiple elements from the back when count given and modifier is RIGHT' do + expect(@redises.lmpop('empty', @list1, @list2, count: 2, modifier: 'RIGHT')).to( + eq([@list1, %w[c b]]) + ) + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[a]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'returns falsed if all lists are empty' do + expect(@redises.lmpop('empty')).to be_nil + + expect(@redises.lrange(@list1, 0, -1)).to eq(%w[a b c]) + expect(@redises.lrange(@list2, 0, -1)).to eq(%w[x y z]) + end + + it 'removes empty lists' do + (@redises.llen(@list1) + @redises.llen(@list2)).times { @redises.lmpop(@list1, @list2) } + expect(@redises.get(@list1)).to be_nil + end + + it 'raises an error for non-list source value' do + @redises.set(@list1, 'string value') + + expect do + @redises.lmpop(@list1, @list2) + end.to raise_error(Redis::CommandError) + end + + it_should_behave_like 'a list-only command' +end