blob: 30df81f27c648fc0592c705e26e06db1836338a6 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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
*
* 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 com.android.server.nearby.common.bloomfilter;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.primitives.Bytes.concat;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import org.junit.Test;
import java.util.Arrays;
/**
* Unit-tests for the {@link BloomFilter} class.
*/
public class BloomFilterTest {
private static final int BYTE_ARRAY_LENGTH = 100;
private final BloomFilter mBloomFilter =
new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
public BloomFilter.Hasher newHasher() {
return new FastPairBloomFilterHasher();
}
@Test
public void emptyFilter_returnsEmptyArray() throws Exception {
assertThat(mBloomFilter.asBytes()).isEqualTo(new byte[BYTE_ARRAY_LENGTH]);
}
@Test
public void emptyFilter_neverContains() throws Exception {
assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
}
@Test
public void add() throws Exception {
assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
mBloomFilter.add(element(1));
assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
}
@Test
public void add_onlyGivenArgAdded() throws Exception {
mBloomFilter.add(element(1));
assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
}
@Test
public void add_multipleArgs() throws Exception {
mBloomFilter.add(element(1));
mBloomFilter.add(element(2));
assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
assertThat(mBloomFilter.possiblyContains(element(2))).isTrue();
assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
}
/**
* This test was added because of a bug where the BloomFilter doesn't utilize all bits given.
* Functionally, the filter still works, but we just have a much higher false positive rate. The
* bug was caused by confusing bit length and byte length, which made our BloomFilter only set
* bits on the first byteLength (bitLength / 8) bits rather than the whole bitLength bits.
*
* <p>Here, we're verifying that the bits set are somewhat scattered. So instead of something
* like [ 0, 1, 1, 0, 0, 0, 0, ..., 0 ], we should be getting something like
* [ 0, 1, 0, 0, 1, 1, 0, 0,0, 1, ..., 1, 0].
*/
@Test
public void randomness_noEndBias() throws Exception {
// Add one element to our BloomFilter.
mBloomFilter.add(element(1));
// Record the amount of non-zero bytes and the longest streak of zero bytes in the resulting
// BloomFilter. This is an approximation of reasonable distribution since we're recording by
// bytes instead of bits.
int nonZeroCount = 0;
int longestZeroStreak = 0;
int currentZeroStreak = 0;
for (byte b : mBloomFilter.asBytes()) {
if (b == 0) {
currentZeroStreak++;
} else {
// Increment the number of non-zero bytes we've seen, update the longest zero
// streak, and then reset the current zero streak.
nonZeroCount++;
longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
currentZeroStreak = 0;
}
}
// Update the longest zero streak again for the tail case.
longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
// Since randomness is hard to measure within one unit test, we instead do a valid check.
// All non-zero bytes should not be packed into one end of the array.
//
// In this case, the size of one end is approximated to be:
// BYTE_ARRAY_LENGTH / nonZeroCount.
// Therefore, the longest zero streak should be less than:
// BYTE_ARRAY_LENGTH - one end of the array.
int longestAcceptableZeroStreak = BYTE_ARRAY_LENGTH - (BYTE_ARRAY_LENGTH / nonZeroCount);
assertThat(longestZeroStreak).isAtMost(longestAcceptableZeroStreak);
}
@Test
public void randomness_falsePositiveRate() throws Exception {
// Create a new BloomFilter with a length of only 10 bytes.
BloomFilter bloomFilter = new BloomFilter(new byte[10], newHasher());
// Add 5 distinct elements to the BloomFilter.
for (int i = 0; i < 5; i++) {
bloomFilter.add(element(i));
}
// Now test 100 other elements and record the number of false positives.
int falsePositives = 0;
for (int i = 5; i < 105; i++) {
falsePositives += bloomFilter.possiblyContains(element(i)) ? 1 : 0;
}
// We expect the false positive rate to be 3% with 5 elements in a 10 byte filter. Thus,
// we give a little leeway and verify that the false positive rate is no more than 5%.
assertWithMessage(
String.format(
"False positive rate too large. Expected <= 5%%, but got %d%%.",
falsePositives))
.that(falsePositives <= 5)
.isTrue();
System.out.printf("False positive rate: %d%%%n", falsePositives);
}
private String element(int index) {
return "ELEMENT_" + index;
}
@Test
public void specificBitPattern() throws Exception {
// Create a new BloomFilter along with a fixed set of elements
// and bit patterns to verify with.
BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
// Combine an account key and mac address.
byte[] element =
concat(
base16().decode("11223344556677889900AABBCCDDEEFF"),
base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
// Add the fixed elements to the filter.
bloomFilter.add(element);
// Verify that the resulting bytes match the expected one.
byte[] bloomFilterBytes = bloomFilter.asBytes();
assertWithMessage(
"Unexpected bit pattern. Expected %s, but got %s.",
base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
.that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
.isTrue();
// Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
assertThat(bloomFilter.possiblyContains(element)).isTrue();
}
// This test case has been on the spec,
// https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
// Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
// address) to clarify test results with partners.
@Test
public void specificBitPattern_hasOneAccountKey() {
BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
byte[] salt1 = base16().decode("C7");
byte[] salt2 = base16().decode("C7C8");
// Add the fixed elements to the filter.
bloomFilter1.add(concat(accountKey, salt1));
bloomFilter2.add(concat(accountKey, salt2));
assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
}
// Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
// address, battery data) to clarify test results with partners.
@Test
public void specificBitPattern_hasOneAccountKey_withBatteryData() {
BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
byte[] salt1 = base16().decode("C7");
byte[] salt2 = base16().decode("C7C8");
byte[] batteryData = {
0b00110011, // length = 3, show UI indication.
0b01000000, // Left bud: not charging, battery level = 64.
0b01000000, // Right bud: not charging, battery level = 64.
0b01000000 // Case: not charging, battery level = 64.
};
// Adds battery data to build bloom filter.
bloomFilter1.add(concat(accountKey, salt1, batteryData));
bloomFilter2.add(concat(accountKey, salt2, batteryData));
assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
}
// This test case has been on the spec,
// https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
// Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
// address) to clarify test results with partners.
@Test
public void specificBitPattern_hasTwoAccountKeys() {
BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
byte[] salt1 = base16().decode("C7");
byte[] salt2 = base16().decode("C7C8");
// Adds the fixed elements to the filter.
bloomFilter1.add(concat(accountKey1, salt1));
bloomFilter1.add(concat(accountKey2, salt1));
bloomFilter2.add(concat(accountKey1, salt2));
bloomFilter2.add(concat(accountKey2, salt2));
assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
}
// Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
// address, battery data) to clarify test results with partners.
@Test
public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
byte[] salt1 = base16().decode("C7");
byte[] salt2 = base16().decode("C7C8");
byte[] batteryData = {
0b00110011, // length = 3, show UI indication.
0b01000000, // Left bud: not charging, battery level = 64.
0b01000000, // Right bud: not charging, battery level = 64.
0b01000000 // Case: not charging, battery level = 64.
};
// Adds battery data to build bloom filter.
bloomFilter1.add(concat(accountKey1, salt1, batteryData));
bloomFilter1.add(concat(accountKey2, salt1, batteryData));
bloomFilter2.add(concat(accountKey1, salt2, batteryData));
bloomFilter2.add(concat(accountKey2, salt2, batteryData));
assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
}
// Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
// address, battery data and battery remaining time) to clarify test results with partners.
@Test
public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
byte[] salt1 = base16().decode("C7");
byte[] salt2 = base16().decode("C7C8");
byte[] batteryData = {
0b00110011, // length = 3, show UI indication.
0b01000000, // Left bud: not charging, battery level = 64.
0b01000000, // Right bud: not charging, battery level = 64.
0b01000000 // Case: not charging, battery level = 64.
};
byte[] batteryRemainingTime = {
0b00010101, // length = 1, type = 0b0101 (remaining battery time).
0x1E, // remaining battery time (in minutes) = 30 minutes.
};
// Adds battery data to build bloom filter.
bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
}
}