This is an automated email from the ASF dual-hosted git repository.
jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 1f668a2 GEODE-9946: Add Radish LREM command (#7431)
1f668a2 is described below
commit 1f668a2fa94ab77172670b22fc8310ef8198b243
Author: Kris10 <[email protected]>
AuthorDate: Thu Mar 24 19:53:15 2022 -0700
GEODE-9946: Add Radish LREM command (#7431)
This implements a version of the Redis LREM command, which is used for
list data types. Associated tests were also added.
For a list stored at a key, lrem removes the first given count
occurrences equal to the element specified. A positive count starts at
the head, while a negative count starts at the tail. If the count is 0,
it removes all occurrences of the element. It returns the amount of
elements actually removed.
ApplyRemoveElementsByIndex delta was modified to remove elements in one
iteration. A new delta was added to remove elements in one iteration
starting from the tail.
---
.../tools_modules/geode_for_redis.html.md.erb | 34 ++--
geode-for-redis/README.md | 1 +
.../list/LRemNativeRedisAcceptanceTest.java | 37 ++++
.../commands/executor/list/LRemDUnitTest.java | 200 +++++++++++++++++++++
.../executor/list/AbstractLRemIntegrationTest.java | 165 +++++++++++++++++
.../executor/list/LRemIntegrationTest.java | 31 ++++
.../server/AbstractHitsMissesIntegrationTest.java | 5 +
.../redis/internal/commands/RedisCommandType.java | 2 +
.../commands/executor/list/LRemExecutor.java | 51 ++++++
.../redis/internal/data/AbstractRedisData.java | 3 +-
.../geode/redis/internal/data/NullRedisList.java | 6 +
.../geode/redis/internal/data/RedisList.java | 39 +++-
.../data/collections/SizeableByteArrayList.java | 71 ++++++++
.../internal/data/delta/RemoveElementsByIndex.java | 5 +
.../geode/redis/internal/data/RedisListTest.java | 46 +++++
.../collections/SizeableByteArrayListTest.java | 58 +++++-
16 files changed, 721 insertions(+), 33 deletions(-)
diff --git a/geode-docs/tools_modules/geode_for_redis.html.md.erb
b/geode-docs/tools_modules/geode_for_redis.html.md.erb
index 1f4346f..2e1a69d0 100644
--- a/geode-docs/tools_modules/geode_for_redis.html.md.erb
+++ b/geode-docs/tools_modules/geode_for_redis.html.md.erb
@@ -189,23 +189,23 @@ Could not connect to Redis at 127.0.0.1:6379: Connection
refused
| INCRBY | INCRBYFLOAT | INFO **[4]** | KEYS
|
| LINDEX | LINSERT | LLEN | LOLWUT
|
| LPOP | LPUSH | LPUSHX | LRANGE
|
-| LSET | MGET | MSET | MSETNX
|
-| PERSIST | PEXPIRE | PEXPIREAT | PING
|
-| PSETEX | PSUBSCRIBE | PTTL | PUBLISH
|
-| PUBSUB | PUNSUBSCRIBE | RENAME | RENAMENX
|
-| RESTORE | RPUSH | RPOP | SADD
|
-| SCARD | SDIFF | SDIFFSTORE | SET
|
-| SETEX | SETNX | SETRANGE | SINTER
|
-| SINTERSTORE | SISMEMBER | SMEMBERS | SMOVE
|
-| SPOP | SRANDMEMBER | SREM | SSCAN **[3]**
|
-| STRLEN | SUBSCRIBE | SUNION | SUNIONSTORE
|
-| TTL | TYPE | UNSUBSCRIBE | QUIT
|
-| ZADD | ZCARD | ZCOUNT | ZINCRBY
|
-| ZINTERSTORE | ZLEXCOUNT | ZPOPMAX | ZPOPMIN
|
-| ZRANGE | ZRANGEBYLEX | ZRANGEBYSCORE | ZRANK
|
-| ZREM | ZREMRANGEBYLEX | ZREMRANGEBYRANK |
ZREMRANGEBYSCORE |
-| ZREVRANGE | ZREVRANGEBYLEX | ZREVRANGEBYSCORE | ZREVRANK
|
-| ZSCAN **[3]** | ZSCORE | ZUNIONSTORE |
|
+| LREM | LSET | MGET | MSET
|
+| MSETNX | PERSIST | PEXPIRE | PEXPIREAT
|
+| PING | PSETEX | PSUBSCRIBE | PTTL
|
+| PUBLISH | PUBSUB | PUNSUBSCRIBE | RENAME
|
+| RENAMENX | RESTORE | RPUSH | RPOP
|
+| SADD | SCARD | SDIFF | SDIFFSTORE
|
+| SET | SETEX | SETNX | SETRANGE
|
+| SINTER | SINTERSTORE | SISMEMBER | SMEMBERS
|
+| SMOVE | SPOP | SRANDMEMBER | SREM
|
+| SSCAN **[3]** | STRLEN | SUBSCRIBE | SUNION
|
+| SUNIONSTORE | TTL | TYPE | UNSUBSCRIBE
|
+| QUIT | ZADD | ZCARD | ZCOUNT
|
+| ZINCRBY | ZINTERSTORE | ZLEXCOUNT | ZPOPMAX
|
+| ZPOPMIN | ZRANGE | ZRANGEBYLEX | ZRANGEBYSCORE
|
+| ZRANK | ZREM | ZREMRANGEBYLEX |
ZREMRANGEBYRANK |
+| ZREMRANGEBYSCORE | ZREVRANGE | ZREVRANGEBYLEX |
ZREVRANGEBYSCORE |
+| ZREVRANK | ZSCAN **[3]** | ZSCORE | ZUNIONSTORE
|
Commands not listed above are **not implemented**.
diff --git a/geode-for-redis/README.md b/geode-for-redis/README.md
index 8c9a9a4..616c4d0 100644
--- a/geode-for-redis/README.md
+++ b/geode-for-redis/README.md
@@ -184,6 +184,7 @@ Geode for Redis implements a subset of the full Redis
command set.
- KEYS
- LINDEX
- LRANGE
+- LREM
- MGET
- MSET
- MSETNX
diff --git
a/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemNativeRedisAcceptanceTest.java
b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemNativeRedisAcceptanceTest.java
new file mode 100755
index 0000000..c0709a6
--- /dev/null
+++
b/geode-for-redis/src/acceptanceTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemNativeRedisAcceptanceTest.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+
+import org.junit.ClassRule;
+
+import org.apache.geode.redis.NativeRedisClusterTestRule;
+
+public class LRemNativeRedisAcceptanceTest extends AbstractLRemIntegrationTest
{
+
+ @ClassRule
+ public static NativeRedisClusterTestRule redis = new
NativeRedisClusterTestRule();
+
+ @Override
+ public int getPort() {
+ return redis.getExposedPorts().get(0);
+ }
+
+ @Override
+ public void flushAll() {
+ redis.flushAll();
+ }
+
+}
diff --git
a/geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemDUnitTest.java
b/geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemDUnitTest.java
new file mode 100644
index 0000000..434b417
--- /dev/null
+++
b/geode-for-redis/src/distributedTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemDUnitTest.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class LRemDUnitTest {
+ private static final int LIST_SIZE_FOR_BUCKET_TEST = 10000;
+ private static final int UNIQUE_ELEMENTS = 5000;
+ // How many times a unique element is repeated in the list
+ private static final int COUNT_OF_UNIQUE_ELEMENT = 2;
+
+
+ @Rule
+ public RedisClusterStartupRule clusterStartUp = new
RedisClusterStartupRule();
+
+ @Rule
+ public ExecutorServiceRule executor = new ExecutorServiceRule();
+
+ private static JedisCluster jedis;
+
+ @Before
+ public void testSetup() {
+ MemberVM locator = clusterStartUp.startLocatorVM(0);
+ clusterStartUp.startRedisVM(1, locator.getPort());
+ clusterStartUp.startRedisVM(2, locator.getPort());
+ clusterStartUp.startRedisVM(3, locator.getPort());
+ int redisServerPort = clusterStartUp.getRedisPort(1);
+ jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, redisServerPort),
REDIS_CLIENT_TIMEOUT);
+ clusterStartUp.flushAll();
+ }
+
+ @After
+ public void tearDown() {
+ jedis.close();
+ }
+
+ @Test
+ public void shouldDistributeDataAmongCluster_andRetainDataAfterServerCrash()
{
+ String key = makeListKeyWithHashtag(1,
clusterStartUp.getKeyOnServer("lrem", 1));
+
+ // Create initial list and push it
+ final int initialListSize = 30;
+ final int uniqueElements = 3;
+ String[] elementList = new String[initialListSize];
+ for (int i = 0; i < initialListSize; i++) {
+ elementList[i] = makeElementString(key, i % uniqueElements);
+ }
+ jedis.lpush(key, elementList);
+
+ // Remove all elements except for ELEMENT_TO_CHECK
+ final int uniqueElementsCount = initialListSize / uniqueElements;
+ assertThat(jedis.lrem(key, 0, makeElementString(key,
0))).isEqualTo(uniqueElementsCount);
+ assertThat(jedis.lrem(key, -uniqueElementsCount, makeElementString(key,
1)))
+ .isEqualTo(uniqueElementsCount);
+
+ clusterStartUp.crashVM(1); // kill primary server
+
+ assertThat(jedis.llen(key)).isEqualTo(uniqueElementsCount);
+ assertThat(jedis.lrem(key, uniqueElementsCount, makeElementString(key, 2)))
+ .isEqualTo(uniqueElementsCount);
+ assertThat(jedis.exists(key)).isFalse();
+ }
+
+
+ @Test
+ public void givenBucketsMoveDuringLrem_thenOperationsAreNotLost() throws
Exception {
+ AtomicBoolean running = new AtomicBoolean(true);
+
+ List<String> listHashtags = makeListHashtags();
+ String key1 = makeListKeyWithHashtag(1, listHashtags.get(0));
+ String key2 = makeListKeyWithHashtag(2, listHashtags.get(1));
+ String key3 = makeListKeyWithHashtag(3, listHashtags.get(2));
+
+ String[] elementList1 = makeListWithRepeatingElements(key1);
+ String[] elementList2 = makeListWithRepeatingElements(key2);
+ String[] elementList3 = makeListWithRepeatingElements(key3);
+
+ jedis.lpush(key1, elementList1);
+ jedis.lpush(key2, elementList2);
+ jedis.lpush(key3, elementList3);
+
+ Future<Integer> future1 =
+ executor.submit(() -> performLremAndVerify(key1, running,
elementList1));
+ Future<Integer> future2 =
+ executor.submit(() -> performLremAndVerify(key2, running,
elementList2));
+ Future<Integer> future3 =
+ executor.submit(() -> performLremAndVerify(key3, running,
elementList3));
+
+ for (int i = 0; i < 50; i++) {
+ clusterStartUp.moveBucketForKey(listHashtags.get(i %
listHashtags.size()));
+ Thread.sleep(500);
+ }
+
+ running.set(false);
+
+ verifyLremResult(key1, future1.get());
+ verifyLremResult(key2, future2.get());
+ verifyLremResult(key3, future3.get());
+ }
+
+ private void verifyLremResult(String key, int iterationCount) {
+ for (int i = UNIQUE_ELEMENTS - 1; i >= iterationCount; i--) {
+ String element = makeElementString(key, i);
+ assertThat(jedis.lrem(key, COUNT_OF_UNIQUE_ELEMENT, element))
+ .isEqualTo(COUNT_OF_UNIQUE_ELEMENT);
+ }
+ assertThat(jedis.exists(key)).isFalse();
+ }
+
+ private Integer performLremAndVerify(String key, AtomicBoolean isRunning,
String[] list) {
+ assertThat(jedis.llen(key)).isEqualTo(LIST_SIZE_FOR_BUCKET_TEST);
+ int count = COUNT_OF_UNIQUE_ELEMENT;
+ List<String> expectedList = getReversedList(list);
+
+ int iterationCount = 0;
+ while (isRunning.get()) {
+ count = -count;
+ String element = makeElementString(key, iterationCount);
+ assertThat(jedis.lrem(key, count,
element)).isEqualTo(COUNT_OF_UNIQUE_ELEMENT);
+
+ expectedList.removeAll(Collections.singleton(element));
+ assertThat(jedis.lrange(key, 0, -1)).isEqualTo(expectedList);
+ iterationCount++;
+
+ if (iterationCount == UNIQUE_ELEMENTS) {
+ iterationCount = 0;
+ jedis.lpush(key, list);
+ expectedList = getReversedList(list);
+ }
+ }
+
+ return iterationCount;
+ }
+
+ private String[] makeListWithRepeatingElements(String key) {
+ String[] elementList = new String[LIST_SIZE_FOR_BUCKET_TEST];
+ for (int i = 0; i < LIST_SIZE_FOR_BUCKET_TEST; i++) {
+ elementList[i] = makeElementString(key, i % UNIQUE_ELEMENTS);
+ }
+ return elementList;
+ }
+
+ private List<String> makeListHashtags() {
+ List<String> listHashtags = new ArrayList<>();
+ listHashtags.add(clusterStartUp.getKeyOnServer("lrem", 1));
+ listHashtags.add(clusterStartUp.getKeyOnServer("lrem", 2));
+ listHashtags.add(clusterStartUp.getKeyOnServer("lrem", 3));
+ return listHashtags;
+ }
+
+ private List<String> getReversedList(String[] list) {
+ int listSize = list.length;
+ List<String> reversedList = new ArrayList<>(listSize);
+ for (int i = LIST_SIZE_FOR_BUCKET_TEST - 1; 0 <= i; i--) {
+ reversedList.add(list[i]);
+ }
+ return reversedList;
+ }
+
+
+ private String makeListKeyWithHashtag(int index, String hashtag) {
+ return "{" + hashtag + "}-key-" + index;
+ }
+
+ private String makeElementString(String key, int iterationCount) {
+ return "-" + key + "-" + iterationCount + "-";
+ }
+}
diff --git
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRemIntegrationTest.java
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRemIntegrationTest.java
new file mode 100755
index 0000000..6b91adc
--- /dev/null
+++
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractLRemIntegrationTest.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static
org.apache.geode.test.dunit.rules.RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.Protocol;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.redis.RedisIntegrationTest;
+
+public abstract class AbstractLRemIntegrationTest implements
RedisIntegrationTest {
+ private static final String NON_EXISTENT_LIST_KEY = "{tag1}nonExistentKey";
+ private static final String LIST_KEY = "{tag1}listKey";
+ private static final String[] LIST_ELEMENTS =
+ {"pause", "cynic", "sugar", "skill", "pause", "pause", "pause", "aroma",
"sugar", "pause",
+ "elder"};
+ private JedisCluster jedis;
+
+ @Before
+ public void setUp() {
+ jedis = new JedisCluster(new HostAndPort(BIND_ADDRESS, getPort()),
REDIS_CLIENT_TIMEOUT);
+ }
+
+ @After
+ public void tearDown() {
+ flushAll();
+ jedis.close();
+ }
+
+ @Test
+ public void lrem_wrongNumberOfArgs_returnsError() {
+ assertExactNumberOfArgs(jedis, Protocol.Command.LREM, 3);
+ }
+
+ @Test
+ public void lrem_withNonExistentList_returnsZero() {
+ assertThat(jedis.lrem(NON_EXISTENT_LIST_KEY, 2, "element")).isEqualTo(0);
+ assertThat(jedis.lrem(NON_EXISTENT_LIST_KEY, -2, "element")).isEqualTo(0);
+ assertThat(jedis.lrem(NON_EXISTENT_LIST_KEY, 0, "element")).isEqualTo(0);
+ }
+
+ @Test
+ public void lrem_withElementNotInList_returnsZero() {
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+ assertThat(jedis.lrem(LIST_KEY, 3, "magic")).isEqualTo(0);
+ }
+
+ @Test
+ public void lrem_withCountAsZero_returnsNumberOfAllMatchingElementsRemoved()
{
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+ final String[] result =
+ {"elder", "sugar", "aroma", "skill", "sugar", "cynic"};
+ assertThat(jedis.lrem(LIST_KEY, 0, "pause")).isEqualTo(5);
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).containsExactly(result);
+ }
+
+ @Test
+ public void lrem_withPositiveCount_returnsNumberOfElementsRemoved() {
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+ // Amount of elements to remove is SMALLER than the amount in the list
+ final String[] result1 =
+ {"elder", "sugar", "aroma", "pause", "skill", "sugar", "cynic",
"pause"};
+ assertThat(jedis.lrem(LIST_KEY, 3, "pause")).isEqualTo(3);
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).containsExactly(result1);
+
+ // Amount of elements to remove is GREATER than the amount in the list
+ final String[] result2 = {"elder", "aroma", "pause", "skill", "cynic",
"pause"};
+ assertThat(jedis.lrem(LIST_KEY, 10, "sugar")).isEqualTo(2);
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).containsExactly(result2);
+ }
+
+ @Test
+ public void lrem_withNegativeCount_returnsNumberOfElementsRemoved() {
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+ // Amount of elements to remove is SMALLER than the amount in the list
+ final String[] result1 =
+ {"elder", "pause", "sugar", "aroma", "pause", "pause", "skill",
"sugar", "cynic"};
+ assertThat(jedis.lrem(LIST_KEY, -2, "pause")).isEqualTo(2);
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).containsExactly(result1);
+
+ // Amount of elements to remove is GREATER than the amount in the list
+ final String[] result2 = {"elder", "sugar", "aroma", "skill", "sugar",
"cynic"};
+ assertThat(jedis.lrem(LIST_KEY, -10, "pause")).isEqualTo(3);
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).containsExactly(result2);
+ }
+
+ @Test
+ public void lrem_withInvalidCount_returnsErrorNotInteger() {
+ // Non Existent List
+ assertThatThrownBy(() -> jedis.sendCommand(NON_EXISTENT_LIST_KEY,
Protocol.Command.LREM,
+ NON_EXISTENT_LIST_KEY, "b", "element")).hasMessage(ERROR_NOT_INTEGER);
+
+ // Existent List
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+ assertThatThrownBy(
+ () -> jedis.sendCommand(LIST_KEY, Protocol.Command.LREM, LIST_KEY,
"b", "element"))
+ .hasMessage(ERROR_NOT_INTEGER);
+ }
+
+ @Test
+ public void lrem_withWrongTypeKey_returnsErrorWrongType() {
+ String key = "{tag1}ding";
+ jedis.set(key, "dong");
+ assertThatThrownBy(() -> jedis.sendCommand(key, Protocol.Command.LREM,
key, "0", "element"))
+ .hasMessage(ERROR_WRONG_TYPE);
+ }
+
+ @Test
+ public void ensureListConsistency_whenRunningConcurrently() {
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+
+ final String[] elementsToAdd = {"pause", "magic", "loved", "pause"};
+ String[] resultPushThenRemove =
+ {"loved", "magic", "elder", "sugar", "aroma", "skill", "sugar",
"cynic"};
+ String[] resultRemoveThenPush =
+ {"pause", "loved", "magic", "pause", "elder", "sugar", "aroma",
"skill", "sugar", "cynic"};
+ final AtomicLong lremResultReference = new AtomicLong();
+ new ConcurrentLoopingThreads(1000,
+ i -> jedis.lpush(LIST_KEY, elementsToAdd),
+ i -> lremResultReference.set(jedis.lrem(LIST_KEY, 0, "pause")))
+ .runWithAction(() -> {
+ // Checks number of elements removed
+ assertThat(lremResultReference).satisfiesAnyOf(
+ lremResult -> assertThat(lremResult.get()).isEqualTo(7),
+ lremResult -> assertThat(lremResult.get()).isEqualTo(5));
+
+ // Checks the elements stored at key
+ assertThat(jedis.lrange(LIST_KEY, 0, -1)).satisfiesAnyOf(
+ elements ->
assertThat(elements).isEqualTo(Arrays.asList(resultPushThenRemove)),
+ elements ->
assertThat(elements).isEqualTo(Arrays.asList(resultRemoveThenPush)));
+ jedis.del(LIST_KEY);
+ jedis.lpush(LIST_KEY, LIST_ELEMENTS);
+ });
+ }
+}
diff --git
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemIntegrationTest.java
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemIntegrationTest.java
new file mode 100755
index 0000000..2249191
--- /dev/null
+++
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/LRemIntegrationTest.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+
+import org.junit.ClassRule;
+
+import org.apache.geode.redis.GeodeRedisServerRule;
+
+public class LRemIntegrationTest extends AbstractLRemIntegrationTest {
+
+ @ClassRule
+ public static GeodeRedisServerRule server = new GeodeRedisServerRule();
+
+ @Override
+ public int getPort() {
+ return server.getPort();
+ }
+}
diff --git
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
index 50000cb..f7daacd 100644
---
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
+++
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
@@ -617,6 +617,11 @@ public abstract class AbstractHitsMissesIntegrationTest
implements RedisIntegrat
}
@Test
+ public void testLrem() {
+ runCommandAndAssertNoStatUpdates(LIST_KEY, k -> jedis.lrem(k, 1,
"element"));
+ }
+
+ @Test
public void testLset() {
runCommandAndAssertNoStatUpdates(LIST_KEY, k -> jedis.lset(k, 0,
"newvalue"));
}
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
index e734c06..22bd028 100755
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
@@ -86,6 +86,7 @@ import
org.apache.geode.redis.internal.commands.executor.list.LPopExecutor;
import org.apache.geode.redis.internal.commands.executor.list.LPushExecutor;
import org.apache.geode.redis.internal.commands.executor.list.LPushXExecutor;
import org.apache.geode.redis.internal.commands.executor.list.LRangeExecutor;
+import org.apache.geode.redis.internal.commands.executor.list.LRemExecutor;
import org.apache.geode.redis.internal.commands.executor.list.LSetExecutor;
import org.apache.geode.redis.internal.commands.executor.list.RPopExecutor;
import org.apache.geode.redis.internal.commands.executor.list.RPushExecutor;
@@ -402,6 +403,7 @@ public enum RedisCommandType {
LPUSHX(new LPushXExecutor(), Category.LIST, SUPPORTED,
new Parameter().min(3).flags(WRITE, DENYOOM, FAST)),
LRANGE(new LRangeExecutor(), Category.LIST, SUPPORTED, new
Parameter().exact(4).flags(READONLY)),
+ LREM(new LRemExecutor(), Category.LIST, SUPPORTED, new
Parameter().exact(4).flags(WRITE)),
LSET(new LSetExecutor(), Category.LIST, SUPPORTED,
new Parameter().exact(4).flags(WRITE, DENYOOM)),
RPUSH(new RPushExecutor(), Category.LIST, SUPPORTED,
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRemExecutor.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRemExecutor.java
new file mode 100644
index 0000000..4f4da94
--- /dev/null
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LRemExecutor.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache
License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the
License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express
+ * or implied. See the License for the specific language governing permissions
and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.commands.executor.list;
+
+import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
+import static org.apache.geode.redis.internal.netty.Coder.bytesToLong;
+import static org.apache.geode.redis.internal.netty.Coder.narrowLongToInt;
+
+import java.util.List;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.redis.internal.commands.Command;
+import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
+import org.apache.geode.redis.internal.commands.executor.RedisResponse;
+import org.apache.geode.redis.internal.data.RedisData;
+import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
+
+public class LRemExecutor implements CommandExecutor {
+
+ @Override
+ public RedisResponse executeCommand(Command command, ExecutionHandlerContext
context) {
+ List<byte[]> commandElems = command.getProcessedCommand();
+ Region<RedisKey, RedisData> region = context.getRegion();
+
+ int count;
+ try {
+ count = narrowLongToInt(bytesToLong(commandElems.get(2)));
+ } catch (NumberFormatException e) {
+ return RedisResponse.error(ERROR_NOT_INTEGER);
+ }
+
+ RedisKey key = command.getKey();
+ int result = context.listLockedExecute(key, false,
+ list -> list.lrem(count, commandElems.get(3), region, key));
+
+ return RedisResponse.integer(result);
+ }
+}
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
index b87cfee..ffcb688 100644
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/AbstractRedisData.java
@@ -425,8 +425,7 @@ public abstract class AbstractRedisData implements
RedisData {
return false;
}
AbstractRedisData that = (AbstractRedisData) o;
- return version == that.version &&
- getExpirationTimestamp() == that.getExpirationTimestamp();
+ return getExpirationTimestamp() == that.getExpirationTimestamp();
}
@Override
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
index f14fc5a..e58aefe 100644
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
@@ -46,11 +46,17 @@ class NullRedisList extends RedisList {
}
@Override
+ public int lrem(int count, byte[] element, Region<RedisKey, RedisData>
region, RedisKey key) {
+ return 0;
+ }
+
+ @Override
public long lpush(ExecutionHandlerContext context, List<byte[]>
elementsToAdd, RedisKey key,
boolean onlyIfExists) {
if (onlyIfExists) {
return 0;
}
+
RedisList newList = new RedisList();
for (byte[] element : elementsToAdd) {
newList.elementPushHead(element);
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
index 37fbfa9..85422fc 100644
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
@@ -59,6 +59,32 @@ public class RedisList extends AbstractRedisData {
}
/**
+ * @param count number of elements to remove.
+ * A count that is 0 removes all matching elements in the list.
+ * Positive count starts from the head and moves to the tail.
+ * Negative count starts from the tail and moves to the head.
+ * @param element element to remove
+ * @param region the region this instance is stored in
+ * @param key the name of the set to add
+ * @return amount of elements that were actually removed
+ */
+ public int lrem(int count, byte[] element, Region<RedisKey, RedisData>
region, RedisKey key) {
+ List<Integer> removedIndexes;
+ byte version;
+ synchronized (this) {
+ removedIndexes = elementList.remove(element, count);
+ version = incrementAndGetVersion();
+ }
+
+ if (!removedIndexes.isEmpty()) {
+ storeChanges(region, key,
+ new RemoveElementsByIndex(version, removedIndexes));
+ }
+
+ return removedIndexes.size();
+ }
+
+ /**
* @param start start index of desired elements
* @param stop stop index of desired elements
* @return list of elements in the range (inclusive).
@@ -100,8 +126,9 @@ public class RedisList extends AbstractRedisData {
}
/**
- * @param index index of desired element. Positive index starts at the head.
Negative index starts
- * at the tail.
+ * @param index index of desired element.
+ * Positive index starts at the head.
+ * Negative index starts at the tail.
* @return element at index. Null if index is out of range.
*/
public byte[] lindex(int index) {
@@ -304,9 +331,11 @@ public class RedisList extends AbstractRedisData {
}
@Override
- public void applyRemoveElementsByIndex(List<Integer> indexes) {
- for (int index : indexes) {
- removeElement(index);
+ public synchronized void applyRemoveElementsByIndex(List<Integer> indexes) {
+ if (indexes.size() == 1) {
+ removeElement(indexes.get(0));
+ } else {
+ elementList.removeIndexes(indexes);
}
}
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
index 1d5446e..78e4b79 100644
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayList.java
@@ -21,6 +21,7 @@ import static
org.apache.geode.internal.JvmSizeUtils.roundUpSize;
import java.util.Arrays;
import java.util.LinkedList;
+import java.util.List;
import java.util.ListIterator;
import org.apache.geode.internal.size.Sizeable;
@@ -31,6 +32,57 @@ public class SizeableByteArrayList extends
LinkedList<byte[]> implements Sizeabl
roundUpSize(getObjectHeaderSize() + 3 * getReferenceSize());
private int memberOverhead;
+ /**
+ * @param toRemove element to remove from the list
+ * @param count number of elements that match object o to remove from the
list.
+ * Count that is equal to 0 removes all matching elements from the
list.
+ * @return list of indexes that were removed in order.
+ */
+ public List<Integer> remove(byte[] elementToRemove, int count) {
+ if (0 <= count) {
+ count = count == 0 ? this.size() : count;
+ return removeObjectsStartingAtHead(elementToRemove, count);
+ } else {
+ return removeObjectsStartingAtTail(elementToRemove, -count);
+ }
+ }
+
+ private List<Integer> removeObjectsStartingAtHead(byte[] elementToRemove,
int count) {
+ int index = 0;
+ ListIterator<byte[]> iterator = listIterator(index);
+ List<Integer> indexesRemoved = new LinkedList<>();
+
+ while (iterator.hasNext() && count != indexesRemoved.size()) {
+ byte[] element = iterator.next();
+ if (Arrays.equals(element, elementToRemove)) {
+ iterator.remove();
+ memberOverhead -= calculateByteArrayOverhead(element);
+ indexesRemoved.add(index);
+ }
+
+ index++;
+ }
+ return indexesRemoved;
+ }
+
+ private List<Integer> removeObjectsStartingAtTail(byte[] elementToRemove,
int count) {
+ int index = size() - 1;
+ ListIterator<byte[]> descendingIterator = listIterator(size());
+ List<Integer> indexesRemoved = new LinkedList<>();
+
+ while (descendingIterator.hasPrevious() && indexesRemoved.size() != count)
{
+ byte[] element = descendingIterator.previous();
+ if (Arrays.equals(element, elementToRemove)) {
+ descendingIterator.remove();
+ memberOverhead -= calculateByteArrayOverhead(element);
+ indexesRemoved.add(0, index);
+ }
+
+ index--;
+ }
+ return indexesRemoved;
+ }
+
@Override
public int indexOf(Object o) {
ListIterator<byte[]> iterator = this.listIterator();
@@ -49,6 +101,25 @@ public class SizeableByteArrayList extends
LinkedList<byte[]> implements Sizeabl
throw new UnsupportedOperationException();
}
+ /**
+ * @param remove in order (smallest to largest) list of indexes to remove
+ */
+ public void removeIndexes(List<Integer> remove) {
+ int removeIndex = 0;
+ int firstIndexToRemove = remove.get(0);
+ ListIterator<byte[]> iterator = listIterator(firstIndexToRemove);
+
+ // Iterates only through the indexes to remove
+ for (int i = firstIndexToRemove; i <= remove.get(remove.size() - 1); i++) {
+ byte[] element = iterator.next();
+ if (i == remove.get(removeIndex)) {
+ iterator.remove();
+ memberOverhead -= calculateByteArrayOverhead(element);
+ removeIndex++;
+ }
+ }
+ }
+
@Override
public boolean remove(Object o) {
ListIterator<byte[]> iterator = this.listIterator();
diff --git
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
index 8c8a7eb..dd3eed3 100644
---
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
+++
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/delta/RemoveElementsByIndex.java
@@ -37,6 +37,11 @@ public class RemoveElementsByIndex extends DeltaInfo {
this.indexes = new ArrayList<>();
}
+ public RemoveElementsByIndex(byte version, List<Integer> indexes) {
+ super(version);
+ this.indexes = indexes;
+ }
+
@Override
public DeltaType getType() {
return REMOVE_ELEMENTS_BY_INDEX;
diff --git
a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
index 712d442..b6b8bab 100644
---
a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
+++
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
@@ -17,16 +17,24 @@
package org.apache.geode.redis.internal.data;
import static
org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_LIST;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Modifier;
import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.serialization.ByteArrayDataInput;
import org.apache.geode.internal.serialization.SerializationContext;
@@ -110,6 +118,44 @@ public class RedisListTest {
assertThat(list2).isEqualTo(list1);
}
+ @Test
+ public void removeIndexes_storesStableDelta() {
+ Region<RedisKey, RedisData> region =
uncheckedCast(mock(PartitionedRegion.class));
+ when(region.put(any(),
any())).thenAnswer(this::validateDeltaSerialization);
+
+ byte[] element = new byte[] {1};
+ RedisList list = createRedisListWithDuplicateElements();
+
+ list.lrem(2, element, region, null);
+
+ verify(region).put(any(), any());
+ assertThat(list.hasDelta()).isFalse();
+ }
+
+ private Object validateDeltaSerialization(InvocationOnMock invocation)
throws IOException {
+ RedisList value = invocation.getArgument(1, RedisList.class);
+ assertThat(value.hasDelta()).isTrue();
+ HeapDataOutputStream out = new HeapDataOutputStream(100);
+ value.toDelta(out);
+ ByteArrayDataInput in = new ByteArrayDataInput(out.toByteArray());
+ RedisList list2 = createRedisListWithDuplicateElements();
+ assertThat(list2).isNotEqualTo(value);
+ list2.fromDelta(in);
+ assertThat(list2).isEqualTo(value);
+ return null;
+ }
+
+
+ private RedisList createRedisListWithDuplicateElements() {
+ RedisList newList = new RedisList();
+ newList.elementPushHead(new byte[] {1});
+ newList.elementPushHead(new byte[] {2});
+ newList.elementPushHead(new byte[] {1});
+ newList.elementPushHead(new byte[] {1});
+ newList.elementPushHead(new byte[] {3});
+ return newList;
+ }
+
private RedisList createRedisList(int e1, int e2) {
RedisList newList = new RedisList();
newList.elementPushHead(new byte[] {(byte) e1});
diff --git
a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
index 18e8629..2cf1ebb 100644
---
a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
+++
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/collections/SizeableByteArrayListTest.java
@@ -16,6 +16,10 @@ package org.apache.geode.redis.internal.data.collections;
import static org.assertj.core.api.Assertions.assertThat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
import org.junit.Test;
import org.apache.geode.cache.util.ObjectSizer;
@@ -23,6 +27,7 @@ import org.apache.geode.internal.size.ReflectionObjectSizer;
public class SizeableByteArrayListTest {
private final ObjectSizer sizer = ReflectionObjectSizer.getInstance();
+ private final int INITIAL_NUMBER_OF_ELEMENTS = 20;
@Test
public void getSizeInBytesIsAccurate_ForEmptySizeableByteArrayList() {
@@ -32,32 +37,67 @@ public class SizeableByteArrayListTest {
@Test
public void getSizeInBytesIsAccurate_ForSizeableByteArrayListElements() {
- int initialNumberOfElements = 20;
int elementsToAdd = 100;
// Create a list with an initial size and confirm that it correctly
reports its size
- SizeableByteArrayList list = new SizeableByteArrayList();
- for (int i = 0; i < initialNumberOfElements; ++i) {
- list.addFirst(makeByteArrayOfSpecifiedLength(i + 1));
- }
+ SizeableByteArrayList list = createList();
assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
// Add elements and assert that the size is correct after each add
- for (int i = initialNumberOfElements; i < initialNumberOfElements +
elementsToAdd; ++i) {
+ for (int i = INITIAL_NUMBER_OF_ELEMENTS; i < INITIAL_NUMBER_OF_ELEMENTS +
elementsToAdd; ++i) {
list.addFirst(makeByteArrayOfSpecifiedLength(i));
assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
}
- assertThat(list.size()).isEqualTo(initialNumberOfElements + elementsToAdd);
+ assertThat(list.size()).isEqualTo(INITIAL_NUMBER_OF_ELEMENTS +
elementsToAdd);
// Remove all the elements and assert that the size is correct after each
remove
- for (int i = 0; i < initialNumberOfElements + elementsToAdd; ++i) {
+ for (int i = 0; i < INITIAL_NUMBER_OF_ELEMENTS + elementsToAdd; ++i) {
list.remove(0);
assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
}
assertThat(list.size()).isEqualTo(0);
}
- byte[] makeByteArrayOfSpecifiedLength(int length) {
+ @Test
+ public void removeObjects_getSizeInBytesIsAccurate() {
+ // Create a list with an initial size and confirm that it correctly
reports its size
+ SizeableByteArrayList list = createList();
+ assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+
+ // Remove all the elements and assert that the size is correct after each
remove
+ Random rand = new Random();
+ for (int i = 0; i < INITIAL_NUMBER_OF_ELEMENTS; ++i) {
+ list.remove(makeByteArrayOfSpecifiedLength(i + 1), rand.nextInt(3) - 1);
+ assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+ }
+ assertThat(list.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void removeIndexes_getSizeInBytesIsAccurate() {
+ // Create a list with an initial size and confirm that it correctly
reports its size
+ SizeableByteArrayList list = createList();
+ assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+
+ // Remove all the elements and assert that the size is correct after each
remove
+ for (int i = INITIAL_NUMBER_OF_ELEMENTS - 1; 0 <= i; --i) {
+ List<Integer> indexToRemove = new ArrayList<>(1);
+ indexToRemove.add(i);
+ list.removeIndexes(indexToRemove);
+ assertThat(list.getSizeInBytes()).isEqualTo(sizer.sizeof(list));
+ }
+ assertThat(list.size()).isEqualTo(0);
+ }
+
+ private SizeableByteArrayList createList() {
+ SizeableByteArrayList list = new SizeableByteArrayList();
+ for (int i = 0; i < INITIAL_NUMBER_OF_ELEMENTS; ++i) {
+ list.addFirst(makeByteArrayOfSpecifiedLength(i + 1));
+ }
+ return list;
+ }
+
+ private byte[] makeByteArrayOfSpecifiedLength(int length) {
byte[] newByteArray = new byte[length];
for (int i = 0; i < length; i++) {
newByteArray[i] = (byte) i;