blob: 56c37981d1bd617487c9d5e514babc15b46a9b1e [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Licensed 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
//
// https://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 com.google.setfilters.cuckoofilter;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.hash.Funnel;
import com.google.common.hash.Funnels;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class CuckooFilterConfigTest {
public static final class TestHashFunction implements CuckooFilterConfig.HashFunction {
@Override
public <T> HashCode hash(T element, Funnel<? super T> funnel) {
return Hashing.murmur3_128().hashObject(element, funnel);
}
}
public static final class TestStrategy implements CuckooFilterConfig.Strategy {
@Override
public long computeFingerprint(HashCode hash, int fingerprintLength) {
return 20;
}
@Override
public int computeBucketIndex(HashCode hash, int bucketCount) {
return 0;
}
@Override
public int computeOtherBucketIndex(
long fingerprint,
int bucketIndex,
int bucketCount,
CuckooFilterConfig.HashFunction hashFunction) {
return 1;
}
}
@Test
public void build_buildsCuckooFilterConfig() {
CuckooFilterConfig config =
CuckooFilterConfig.newBuilder()
.setSize(
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(100)
.setBucketCapacity(4)
.setFingerprintLength(16)
.build())
.setHashFunction(new TestHashFunction())
.setStrategy(new TestStrategy())
.setUseSpaceOptimization(true)
.build();
CuckooFilterConfig.Size size = config.size();
assertThat(size.bucketCount()).isEqualTo(100);
assertThat(size.bucketCapacity()).isEqualTo(4);
assertThat(size.fingerprintLength()).isEqualTo(16);
Funnel<Long> funnel = Funnels.longFunnel();
CuckooFilterConfig.HashFunction hashFunction = config.hashFunction();
assertThat(hashFunction.hash(100L, funnel))
.isEqualTo(Hashing.murmur3_128().hashObject(100L, funnel));
CuckooFilterConfig.Strategy strategy = config.strategy();
HashCode randomHash = HashCode.fromLong(100L);
assertThat(strategy.computeFingerprint(randomHash, 16)).isEqualTo(20);
assertThat(strategy.computeBucketIndex(randomHash, 100)).isEqualTo(0);
assertThat(strategy.computeOtherBucketIndex(0, 5, 100, config.hashFunction())).isEqualTo(1);
assertThat(strategy.maxReplacementCount()).isEqualTo(500);
assertThat(config.useSpaceOptimization()).isTrue();
}
@Test
public void build_failsWithUnsetSize() {
String message =
assertThrows(IllegalArgumentException.class, () -> CuckooFilterConfig.newBuilder().build())
.getMessage();
assertThat(message).isEqualTo("Size must be set.");
}
@Test
public void build_failsWithUnsetHashFunction() {
String message =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.newBuilder()
.setSize(
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(100)
.setBucketCapacity(4)
.setFingerprintLength(16)
.build())
.build())
.getMessage();
assertThat(message).isEqualTo("Hash function must be set.");
}
@Test
public void build_failsWithUnsetStrategy() {
String message =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.newBuilder()
.setSize(
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(100)
.setBucketCapacity(4)
.setFingerprintLength(16)
.build())
.setHashFunction(new TestHashFunction())
.build())
.getMessage();
assertThat(message).isEqualTo("Strategy must be set.");
}
@Test
public void buildSize_failsWithInvalidBucketCount() {
String message =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.newBuilder().setBucketCount(0).build())
.getMessage();
assertThat(message).isEqualTo("bucketCount must be > 0: 0 given instead.");
}
@Test
public void buildSize_failsWithInvalidBucketCapacity() {
String messageLower =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(1)
.setBucketCapacity(0)
.build())
.getMessage();
assertThat(messageLower)
.isEqualTo("bucketCapacity must be in range (0, 128]: 0 given instead.");
String messageHigher =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(1)
.setBucketCapacity(129)
.build())
.getMessage();
assertThat(messageHigher)
.isEqualTo("bucketCapacity must be in range (0, 128]: 129 given instead.");
}
@Test
public void buildSize_failsWithInvalidFingerprintLength() {
String messageLower =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(1)
.setBucketCapacity(1)
.setFingerprintLength(0)
.build())
.getMessage();
assertThat(messageLower)
.isEqualTo("fingerprintLength must be in range (0, 64]: 0 given instead.");
String messageHigher =
assertThrows(
IllegalArgumentException.class,
() ->
CuckooFilterConfig.Size.newBuilder()
.setBucketCount(1)
.setBucketCapacity(1)
.setFingerprintLength(65)
.build())
.getMessage();
assertThat(messageHigher)
.isEqualTo("fingerprintLength must be in range (0, 64]: 65 given instead.");
}
@Test
public void computeEfficientSize_failsWithInvalidFalsePositiveRate() {
String messageLower =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.computeEfficientSize(0, 5))
.getMessage();
assertThat(messageLower)
.isEqualTo("targetFalsePositiveRate must be in range (0, 1): 0.0 given.");
String messageHigher =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.computeEfficientSize(1, 5))
.getMessage();
assertThat(messageHigher)
.isEqualTo("targetFalsePositiveRate must be in range (0, 1): 1.0 given.");
}
@Test
public void computeEfficientSize_failsWithInvalidElementsCountUpperBound() {
String message =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.computeEfficientSize(0.5, 0))
.getMessage();
assertThat(message).isEqualTo("elementsCountUpperBound must be > 0: 0 given.");
}
@Test
public void computeEfficientSize_failsIfElementsCountUpperBoundTooBig() {
String message =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.computeEfficientSize(0.5, 5000L * Integer.MAX_VALUE))
.getMessage();
assertThat(message)
.isEqualTo(
"Could not compute suitable cuckoo filter size based on the given input. Either the"
+ " target false positive rate is too low, or the computed size is too big.");
}
@Test
public void computeEfficientSize_failsIfFalsePositiveRateTooLow() {
String message =
assertThrows(
IllegalArgumentException.class,
() -> CuckooFilterConfig.Size.computeEfficientSize(Double.MIN_NORMAL, 100))
.getMessage();
assertThat(message)
.isEqualTo(
"Could not compute suitable cuckoo filter size based on the given input. Either the"
+ " target false positive rate is too low, or the computed size is too big.");
}
}