/* * 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.beam.sdk.io.range; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests for {@link ByteKeyRange}. */ @RunWith(JUnit4.class) public class ByteKeyRangeTest { // A set of ranges for testing. private static final ByteKeyRange RANGE_1_10 = ByteKeyRange.of(ByteKey.of(1), ByteKey.of(10)); private static final ByteKeyRange RANGE_5_10 = ByteKeyRange.of(ByteKey.of(5), ByteKey.of(10)); private static final ByteKeyRange RANGE_5_50 = ByteKeyRange.of(ByteKey.of(5), ByteKey.of(50)); private static final ByteKeyRange RANGE_10_50 = ByteKeyRange.of(ByteKey.of(10), ByteKey.of(50)); private static final ByteKeyRange UP_TO_1 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(1)); private static final ByteKeyRange UP_TO_5 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(5)); private static final ByteKeyRange UP_TO_10 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(10)); private static final ByteKeyRange UP_TO_50 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(50)); private static final ByteKeyRange AFTER_1 = ByteKeyRange.of(ByteKey.of(1), ByteKey.EMPTY); private static final ByteKeyRange AFTER_5 = ByteKeyRange.of(ByteKey.of(5), ByteKey.EMPTY); private static final ByteKeyRange AFTER_10 = ByteKeyRange.of(ByteKey.of(10), ByteKey.EMPTY); private static final ByteKeyRange[] TEST_RANGES = new ByteKeyRange[] { ByteKeyRange.ALL_KEYS, RANGE_1_10, RANGE_5_10, RANGE_5_50, RANGE_10_50, UP_TO_1, UP_TO_5, UP_TO_10, UP_TO_50, AFTER_1, AFTER_5, AFTER_10, }; static final ByteKey[] RANGE_TEST_KEYS = ImmutableList.<ByteKey>builder() .addAll(Arrays.asList(ByteKeyTest.TEST_KEYS)) .add(ByteKey.EMPTY) .build() .toArray(ByteKeyTest.TEST_KEYS); /** * Tests that the two ranges do not overlap, passing each in as the first range in the comparison. */ private static void bidirectionalNonOverlap(ByteKeyRange left, ByteKeyRange right) { bidirectionalOverlapHelper(left, right, false); } /** * Tests that the two ranges overlap, passing each in as the first range in the comparison. */ private static void bidirectionalOverlap(ByteKeyRange left, ByteKeyRange right) { bidirectionalOverlapHelper(left, right, true); } /** * Helper function for tests with a good error message. */ private static void bidirectionalOverlapHelper( ByteKeyRange left, ByteKeyRange right, boolean result) { assertEquals(String.format("%s overlaps %s", left, right), result, left.overlaps(right)); assertEquals(String.format("%s overlaps %s", right, left), result, right.overlaps(left)); } /** * Tests of {@link ByteKeyRange#overlaps(ByteKeyRange)} with cases that should return true. */ @Test public void testOverlappingRanges() { bidirectionalOverlap(ByteKeyRange.ALL_KEYS, ByteKeyRange.ALL_KEYS); bidirectionalOverlap(ByteKeyRange.ALL_KEYS, RANGE_1_10); bidirectionalOverlap(UP_TO_1, UP_TO_1); bidirectionalOverlap(UP_TO_1, UP_TO_5); bidirectionalOverlap(UP_TO_50, AFTER_10); bidirectionalOverlap(UP_TO_50, RANGE_1_10); bidirectionalOverlap(UP_TO_10, UP_TO_50); bidirectionalOverlap(RANGE_1_10, RANGE_5_50); bidirectionalOverlap(AFTER_1, AFTER_5); bidirectionalOverlap(RANGE_5_10, RANGE_1_10); bidirectionalOverlap(RANGE_5_10, RANGE_5_50); } /** * Tests of {@link ByteKeyRange#overlaps(ByteKeyRange)} with cases that should return false. */ @Test public void testNonOverlappingRanges() { bidirectionalNonOverlap(UP_TO_1, AFTER_1); bidirectionalNonOverlap(UP_TO_1, AFTER_5); bidirectionalNonOverlap(RANGE_5_10, RANGE_10_50); } /** * Verifies that all keys in the given list are strictly ordered by size. */ private static void ensureOrderedKeys(List<ByteKey> keys) { for (int i = 0; i < keys.size() - 1; ++i) { // This will throw if these two keys do not form a valid range. ByteKeyRange.of(keys.get(i), keys.get(i + 1)); // Also, a key is only allowed empty if it is the first key. if (i > 0 && keys.get(i).isEmpty()) { fail(String.format("Intermediate key %s/%s may not be empty", i, keys.size())); } } } /** Tests for {@link ByteKeyRange#split(int)} with invalid inputs. */ @Test public void testRejectsInvalidSplit() { try { fail(String.format("%s.split(0) should fail: %s", RANGE_1_10, RANGE_1_10.split(0))); } catch (IllegalArgumentException expected) { // pass } try { fail(String.format("%s.split(-3) should fail: %s", RANGE_1_10, RANGE_1_10.split(-3))); } catch (IllegalArgumentException expected) { // pass } } /** Tests for {@link ByteKeyRange#split(int)} with weird inputs. */ @Test public void testSplitSpecialInputs() { // Range split by 1 returns list of its keys. assertEquals( "Split 1 should return input", ImmutableList.of(RANGE_1_10.getStartKey(), RANGE_1_10.getEndKey()), RANGE_1_10.split(1)); // Unsplittable range returns list of its keys. ByteKeyRange unsplittable = ByteKeyRange.of(ByteKey.of(), ByteKey.of(0, 0, 0, 0)); assertEquals( "Unsplittable should return input", ImmutableList.of(unsplittable.getStartKey(), unsplittable.getEndKey()), unsplittable.split(5)); } /** Tests for {@link ByteKeyRange#split(int)}. */ @Test public void testSplitKeysCombinatorial() { List<Integer> sizes = ImmutableList.of(1, 2, 5, 10, 25, 32, 64); for (int i = 0; i < RANGE_TEST_KEYS.length; ++i) { for (int j = i + 1; j < RANGE_TEST_KEYS.length; ++j) { ByteKeyRange range = ByteKeyRange.of(RANGE_TEST_KEYS[i], RANGE_TEST_KEYS[j]); for (int s : sizes) { List<ByteKey> splits = range.split(s); ensureOrderedKeys(splits); assertThat("At least two entries in splits", splits.size(), greaterThanOrEqualTo(2)); assertEquals("First split equals start of range", splits.get(0), RANGE_TEST_KEYS[i]); assertEquals( "Last split equals end of range", splits.get(splits.size() - 1), RANGE_TEST_KEYS[j]); } } } } /** Manual tests for {@link ByteKeyRange#estimateFractionForKey}. */ @Test public void testEstimateFractionForKey() { final double delta = 0.0000001; /* 0x80 is halfway between [] and [] */ assertEquals(0.5, ByteKeyRange.ALL_KEYS.estimateFractionForKey(ByteKey.of(0x80)), delta); /* 0x80 is halfway between [00] and [] */ ByteKeyRange after0 = ByteKeyRange.of(ByteKey.of(0), ByteKey.EMPTY); assertEquals(0.5, after0.estimateFractionForKey(ByteKey.of(0x80)), delta); /* 0x80 is halfway between [0000] and [] */ ByteKeyRange after00 = ByteKeyRange.of(ByteKey.of(0, 0), ByteKey.EMPTY); assertEquals(0.5, after00.estimateFractionForKey(ByteKey.of(0x80)), delta); /* 0x7f is halfway between [] and [fe] */ ByteKeyRange upToFE = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(0xfe)); assertEquals(0.5, upToFE.estimateFractionForKey(ByteKey.of(0x7f)), delta); /* 0x40 is one-quarter of the way between [] and [] */ assertEquals(0.25, ByteKeyRange.ALL_KEYS.estimateFractionForKey(ByteKey.of(0x40)), delta); /* 0x40 is one-half of the way between [] and [0x80] */ ByteKeyRange upTo80 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(0x80)); assertEquals(0.50, upTo80.estimateFractionForKey(ByteKey.of(0x40)), delta); /* 0x40 is one-half of the way between [0x30] and [0x50] */ ByteKeyRange range30to50 = ByteKeyRange.of(ByteKey.of(0x30), ByteKey.of(0x50)); assertEquals(0.50, range30to50.estimateFractionForKey(ByteKey.of(0x40)), delta); /* 0x40 is one-half of the way between [0x30, 0, 1] and [0x4f, 0xff, 0xff, 0, 0] */ ByteKeyRange range31to4f = ByteKeyRange.of(ByteKey.of(0x30, 0, 1), ByteKey.of(0x4f, 0xff, 0xff, 0, 0)); assertEquals(0.50, range31to4f.estimateFractionForKey(ByteKey.of(0x40)), delta); /* Exact fractions from 0 to 47 for a prime range. */ ByteKeyRange upTo47 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(47)); for (int i = 0; i <= 47; ++i) { assertEquals("i=" + i, i / 47.0, upTo47.estimateFractionForKey(ByteKey.of(i)), delta); } /* Exact fractions from 0 to 83 for a prime range. */ ByteKeyRange rangeFDECtoFDEC83 = ByteKeyRange.of(ByteKey.of(0xfd, 0xec), ByteKey.of(0xfd, 0xec, 83)); for (int i = 0; i <= 83; ++i) { assertEquals( "i=" + i, i / 83.0, rangeFDECtoFDEC83.estimateFractionForKey(ByteKey.of(0xfd, 0xec, i)), delta); } } /** Manual tests for {@link ByteKeyRange#interpolateKey}. */ @Test public void testInterpolateKey() { /* 0x80 is halfway between [] and [] */ assertEqualExceptPadding(ByteKey.of(0x80), ByteKeyRange.ALL_KEYS.interpolateKey(0.5)); /* 0x80 is halfway between [00] and [] */ ByteKeyRange after0 = ByteKeyRange.of(ByteKey.of(0), ByteKey.EMPTY); assertEqualExceptPadding(ByteKey.of(0x80), after0.interpolateKey(0.5)); /* 0x80 is halfway between [0000] and [] -- padding to longest key */ ByteKeyRange after00 = ByteKeyRange.of(ByteKey.of(0, 0), ByteKey.EMPTY); assertEqualExceptPadding(ByteKey.of(0x80), after00.interpolateKey(0.5)); /* 0x7f is halfway between [] and [fe] */ ByteKeyRange upToFE = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(0xfe)); assertEqualExceptPadding(ByteKey.of(0x7f), upToFE.interpolateKey(0.5)); /* 0x40 is one-quarter of the way between [] and [] */ assertEqualExceptPadding(ByteKey.of(0x40), ByteKeyRange.ALL_KEYS.interpolateKey(0.25)); /* 0x40 is halfway between [] and [0x80] */ ByteKeyRange upTo80 = ByteKeyRange.of(ByteKey.EMPTY, ByteKey.of(0x80)); assertEqualExceptPadding(ByteKey.of(0x40), upTo80.interpolateKey(0.5)); /* 0x40 is halfway between [0x30] and [0x50] */ ByteKeyRange range30to50 = ByteKeyRange.of(ByteKey.of(0x30), ByteKey.of(0x50)); assertEqualExceptPadding(ByteKey.of(0x40), range30to50.interpolateKey(0.5)); /* 0x40 is halfway between [0x30, 0, 1] and [0x4f, 0xff, 0xff, 0, 0] */ ByteKeyRange range31to4f = ByteKeyRange.of(ByteKey.of(0x30, 0, 1), ByteKey.of(0x4f, 0xff, 0xff, 0, 0)); assertEqualExceptPadding(ByteKey.of(0x40), range31to4f.interpolateKey(0.5)); } /** Tests that {@link ByteKeyRange#interpolateKey} does not return the empty key. */ @Test public void testInterpolateKeyIsNotEmpty() { String fmt = "Interpolating %s at fraction 0.0 should not return the empty key"; for (ByteKeyRange range : TEST_RANGES) { range = ByteKeyRange.ALL_KEYS; assertFalse(String.format(fmt, range), range.interpolateKey(0.0).isEmpty()); } } /** Test {@link ByteKeyRange} getters. */ @Test public void testKeyGetters() { // [1,) assertEquals(AFTER_1.getStartKey(), ByteKey.of(1)); assertEquals(AFTER_1.getEndKey(), ByteKey.EMPTY); // [1, 10) assertEquals(RANGE_1_10.getStartKey(), ByteKey.of(1)); assertEquals(RANGE_1_10.getEndKey(), ByteKey.of(10)); // [, 10) assertEquals(UP_TO_10.getStartKey(), ByteKey.EMPTY); assertEquals(UP_TO_10.getEndKey(), ByteKey.of(10)); } /** Test {@link ByteKeyRange#toString}. */ @Test public void testToString() { assertEquals("ByteKeyRange{startKey=[], endKey=[0a]}", UP_TO_10.toString()); } /** Test {@link ByteKeyRange#equals}. */ @Test public void testEquals() { // Verify that the comparison gives the correct result for all values in both directions. for (int i = 0; i < TEST_RANGES.length; ++i) { for (int j = 0; j < TEST_RANGES.length; ++j) { ByteKeyRange left = TEST_RANGES[i]; ByteKeyRange right = TEST_RANGES[j]; boolean eq = left.equals(right); if (i == j) { assertTrue(String.format("Expected that %s is equal to itself.", left), eq); assertTrue( String.format("Expected that %s is equal to a copy of itself.", left), left.equals(ByteKeyRange.of(right.getStartKey(), right.getEndKey()))); } else { assertFalse(String.format("Expected that %s is not equal to %s", left, right), eq); } } } } /** Test that {@link ByteKeyRange#of} rejects invalid ranges. */ @Test public void testRejectsInvalidRanges() { ByteKey[] testKeys = ByteKeyTest.TEST_KEYS; for (int i = 0; i < testKeys.length; ++i) { for (int j = i; j < testKeys.length; ++j) { if (testKeys[i].isEmpty() || testKeys[j].isEmpty()) { continue; // these are valid ranges. } try { ByteKeyRange range = ByteKeyRange.of(testKeys[j], testKeys[i]); fail(String.format("Expected failure constructing %s", range)); } catch (IllegalArgumentException expected) { // pass } } } } /** Test {@link ByteKeyRange#hashCode}. */ @Test public void testHashCode() { // Verify that the hashCode is equal when i==j, and usually not equal otherwise. int collisions = 0; for (int i = 0; i < TEST_RANGES.length; ++i) { ByteKeyRange current = TEST_RANGES[i]; int left = current.hashCode(); int leftClone = ByteKeyRange.of(current.getStartKey(), current.getEndKey()).hashCode(); assertEquals( String.format("Expected same hash code for %s and a copy of itself", current), left, leftClone); for (int j = i + 1; j < TEST_RANGES.length; ++j) { int right = TEST_RANGES[j].hashCode(); if (left == right) { ++collisions; } } } int totalUnequalTests = TEST_RANGES.length * (TEST_RANGES.length - 1) / 2; assertThat("Too many hash collisions", collisions, lessThan(totalUnequalTests / 2)); } /** Asserts the two keys are equal except trailing zeros. */ private static void assertEqualExceptPadding(ByteKey expected, ByteKey key) { ByteBuffer shortKey = expected.getValue(); ByteBuffer longKey = key.getValue(); if (shortKey.remaining() > longKey.remaining()) { shortKey = key.getValue(); longKey = expected.getValue(); } for (int i = 0; i < shortKey.remaining(); ++i) { if (shortKey.get(i) != longKey.get(i)) { fail(String.format("Expected %s (up to trailing zeros), got %s", expected, key)); } } for (int j = shortKey.remaining(); j < longKey.remaining(); ++j) { if (longKey.get(j) != 0) { fail(String.format("Expected %s (up to trailing zeros), got %s", expected, key)); } } } }