1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.setfilters.cuckoofilter; 16 17 import static com.google.common.truth.Truth.assertThat; 18 import static com.google.common.truth.Truth8.assertThat; 19 import static org.junit.Assert.assertThrows; 20 import static org.mockito.Mockito.mock; 21 import static org.mockito.Mockito.when; 22 23 import java.util.Arrays; 24 import java.util.List; 25 import java.util.Optional; 26 import java.util.Random; 27 import org.junit.Before; 28 import org.junit.Test; 29 import org.junit.runner.RunWith; 30 import org.junit.runners.Parameterized; 31 import org.junit.runners.Parameterized.Parameter; 32 import org.junit.runners.Parameterized.Parameters; 33 34 @RunWith(Parameterized.class) 35 public final class CuckooFilterTableTest { 36 private static final int BUCKET_COUNT = 10000; 37 private static final int BUCKET_CAPACITY = 4; 38 private static final int FINGERPRINT_LENGTH = 16; 39 40 private Random random; 41 private CuckooFilterTable table; 42 43 private interface CuckooFilterTableFactory { create(CuckooFilterConfig.Size size, Random random)44 public CuckooFilterTable create(CuckooFilterConfig.Size size, Random random); 45 createExisting( SerializedCuckooFilterTable serializedTable, Random random)46 public default CuckooFilterTable createExisting( 47 SerializedCuckooFilterTable serializedTable, Random random) { 48 return CuckooFilterTable.createFromSerialization(serializedTable, random); 49 } 50 } 51 52 private static class SemiSortedCuckooFilterTableFactory implements CuckooFilterTableFactory { 53 @Override create(CuckooFilterConfig.Size size, Random random)54 public CuckooFilterTable create(CuckooFilterConfig.Size size, Random random) { 55 return new SemiSortedCuckooFilterTable(size, random); 56 } 57 } 58 59 private static class UncompressedCuckooFilterTableFactory implements CuckooFilterTableFactory { 60 @Override create(CuckooFilterConfig.Size size, Random random)61 public CuckooFilterTable create(CuckooFilterConfig.Size size, Random random) { 62 return new UncompressedCuckooFilterTable(size, random); 63 } 64 } 65 66 @Parameters data()67 public static List<? extends Object> data() { 68 return Arrays.asList( 69 new SemiSortedCuckooFilterTableFactory(), new UncompressedCuckooFilterTableFactory()); 70 } 71 72 @Parameter public CuckooFilterTableFactory tableFactory; 73 74 @Before setUp()75 public void setUp() { 76 random = mock(Random.class); 77 table = 78 tableFactory.create( 79 CuckooFilterConfig.Size.newBuilder() 80 .setBucketCount(BUCKET_COUNT) 81 .setBucketCapacity(BUCKET_CAPACITY) 82 .setFingerprintLength(FINGERPRINT_LENGTH) 83 .build(), 84 random); 85 } 86 87 @Test insertWithReplacement()88 public void insertWithReplacement() { 89 for (int i = 0; i < BUCKET_COUNT; i++) { 90 long offset = (long) i * BUCKET_CAPACITY; 91 for (int j = 0; j < BUCKET_CAPACITY; j++) { 92 assertThat(table.insertWithReplacement(i, offset + j + 1)).isEmpty(); 93 } 94 when(random.nextInt(BUCKET_CAPACITY)).thenReturn(0); 95 96 Optional<Long> replaced = table.insertWithReplacement(i, offset + BUCKET_CAPACITY + 1); 97 98 boolean anyOf = false; 99 for (int j = 0; j < BUCKET_CAPACITY; j++) { 100 anyOf = anyOf || (replaced.get() == offset + j + 1); 101 } 102 assertThat(anyOf).isTrue(); 103 assertThat(table.contains(i, replaced.get())).isFalse(); 104 for (long fingerprint = offset + 1; 105 fingerprint < offset + BUCKET_CAPACITY + 2; 106 fingerprint++) { 107 if (fingerprint != replaced.get()) { 108 assertThat(table.contains(i, fingerprint)).isTrue(); 109 } 110 } 111 } 112 } 113 114 @Test contains_containsFingerprint()115 public void contains_containsFingerprint() { 116 assertThat(table.insertWithReplacement(0, 1L)).isEmpty(); 117 118 assertThat(table.contains(0, 1L)).isTrue(); 119 } 120 121 @Test contains_doesNotContainFingerprint()122 public void contains_doesNotContainFingerprint() { 123 assertThat(table.contains(0, 1L)).isFalse(); 124 } 125 126 @Test delete_deletesExistingFingerprint()127 public void delete_deletesExistingFingerprint() { 128 assertThat(table.insertWithReplacement(0, 1L)).isEmpty(); 129 assertThat(table.contains(0, 1L)).isTrue(); 130 131 assertThat(table.delete(0, 1L)).isTrue(); 132 assertThat(table.contains(0, 1L)).isFalse(); 133 } 134 135 @Test delete_deletesOneFingerprintAtATime()136 public void delete_deletesOneFingerprintAtATime() { 137 assertThat(table.insertWithReplacement(0, 1L)).isEmpty(); 138 assertThat(table.insertWithReplacement(0, 1L)).isEmpty(); 139 assertThat(table.contains(0, 1L)).isTrue(); 140 141 assertThat(table.delete(0, 1L)).isTrue(); 142 assertThat(table.contains(0, 1L)).isTrue(); 143 assertThat(table.delete(0, 1L)).isTrue(); 144 assertThat(table.contains(0, 1L)).isFalse(); 145 } 146 147 @Test delete_deletesNonExistingFingerprint()148 public void delete_deletesNonExistingFingerprint() { 149 assertThat(table.delete(0, 1L)).isFalse(); 150 } 151 152 @Test isFull()153 public void isFull() { 154 for (int j = 0; j < BUCKET_CAPACITY; j++) { 155 assertThat(table.isFull(0)).isFalse(); 156 assertThat(table.insertWithReplacement(0, j + 1)).isEmpty(); 157 } 158 assertThat(table.isFull(0)).isTrue(); 159 } 160 161 @Test size()162 public void size() { 163 CuckooFilterConfig.Size size = table.size(); 164 165 assertThat(size.bucketCount()).isEqualTo(BUCKET_COUNT); 166 assertThat(size.bucketCapacity()).isEqualTo(BUCKET_CAPACITY); 167 assertThat(size.fingerprintLength()).isEqualTo(FINGERPRINT_LENGTH); 168 } 169 170 @Test serializeAndDeserialize()171 public void serializeAndDeserialize() { 172 for (int i = 0; i < BUCKET_CAPACITY; i++) { 173 long offset = (long) i * BUCKET_CAPACITY; 174 for (int j = 0; j < BUCKET_CAPACITY; j++) { 175 assertThat(table.insertWithReplacement(i, offset + j + 1)).isEmpty(); 176 } 177 } 178 179 SerializedCuckooFilterTable serializedTable = table.serialize(); 180 CuckooFilterTable existingTable = tableFactory.createExisting(serializedTable, new Random()); 181 182 for (int i = 0; i < BUCKET_CAPACITY; i++) { 183 long offset = (long) i * BUCKET_CAPACITY; 184 for (int j = 0; j < BUCKET_CAPACITY; j++) { 185 assertThat(existingTable.contains(i, offset + j + 1)).isTrue(); 186 } 187 } 188 } 189 190 @Test deserialize_failsWithInvalidSerialization()191 public void deserialize_failsWithInvalidSerialization() { 192 SerializedCuckooFilterTable serializedTable = 193 SerializedCuckooFilterTable.createFromByteArray(new byte[12]); 194 195 String message = 196 assertThrows( 197 IllegalArgumentException.class, 198 () -> tableFactory.createExisting(serializedTable, new Random())) 199 .getMessage(); 200 assertThat(message).isEqualTo("Unable to parse the SerializedCuckooFilterTable."); 201 } 202 } 203