Home | History | Annotate | Download | only in hash
      1 /*
      2  * Copyright (C) 2011 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.hash;
     18 
     19 import static com.google.common.base.Charsets.UTF_16LE;
     20 
     21 import com.google.common.collect.Iterables;
     22 import com.google.common.collect.Lists;
     23 import com.google.common.hash.AbstractStreamingHashFunction.AbstractStreamingHasher;
     24 import com.google.common.hash.HashTestUtils.RandomHasherAction;
     25 import com.google.common.jdk5backport.Arrays;
     26 
     27 import junit.framework.TestCase;
     28 
     29 import java.io.ByteArrayOutputStream;
     30 import java.io.UnsupportedEncodingException;
     31 import java.nio.ByteBuffer;
     32 import java.nio.ByteOrder;
     33 import java.nio.charset.Charset;
     34 import java.util.Collections;
     35 import java.util.List;
     36 import java.util.Random;
     37 
     38 /**
     39  * Tests for AbstractStreamingHasher.
     40  *
     41  * @author Dimitris Andreou
     42  */
     43 public class AbstractStreamingHasherTest extends TestCase {
     44   public void testBytes() {
     45     Sink sink = new Sink(4); // byte order insignificant here
     46     byte[] expected = { 1, 2, 3, 4, 5, 6, 7, 8 };
     47     sink.putByte((byte) 1);
     48     sink.putBytes(new byte[] { 2, 3, 4, 5, 6 });
     49     sink.putByte((byte) 7);
     50     sink.putBytes(new byte[] {});
     51     sink.putBytes(new byte[] { 8 });
     52     sink.hash();
     53     sink.assertInvariants(8);
     54     sink.assertBytes(expected);
     55   }
     56 
     57   public void testShort() {
     58     Sink sink = new Sink(4);
     59     sink.putShort((short) 0x0201);
     60     sink.hash();
     61     sink.assertInvariants(2);
     62     sink.assertBytes(new byte[] { 1, 2, 0, 0 }); // padded with zeros
     63   }
     64 
     65   public void testInt() {
     66     Sink sink = new Sink(4);
     67     sink.putInt(0x04030201);
     68     sink.hash();
     69     sink.assertInvariants(4);
     70     sink.assertBytes(new byte[] { 1, 2, 3, 4 });
     71   }
     72 
     73   public void testLong() {
     74     Sink sink = new Sink(8);
     75     sink.putLong(0x0807060504030201L);
     76     sink.hash();
     77     sink.assertInvariants(8);
     78     sink.assertBytes(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
     79   }
     80 
     81   public void testChar() {
     82     Sink sink = new Sink(4);
     83     sink.putChar((char) 0x0201);
     84     sink.hash();
     85     sink.assertInvariants(2);
     86     sink.assertBytes(new byte[] { 1, 2, 0, 0  }); // padded with zeros
     87   }
     88 
     89   public void testString() throws UnsupportedEncodingException {
     90     Random random = new Random();
     91     for (int i = 0; i < 100; i++) {
     92       byte[] bytes = new byte[64];
     93       random.nextBytes(bytes);
     94       String s = new String(bytes, UTF_16LE.name()); // so all random strings are valid
     95       assertEquals(
     96           new Sink(4).putString(s).hash(),
     97           new Sink(4).putBytes(s.getBytes(UTF_16LE.name())).hash());
     98       assertEquals(
     99           new Sink(4).putString(s).hash(),
    100           new Sink(4).putString(s, UTF_16LE).hash());
    101     }
    102   }
    103 
    104   public void testFloat() {
    105     Sink sink = new Sink(4);
    106     sink.putFloat(Float.intBitsToFloat(0x04030201));
    107     sink.hash();
    108     sink.assertInvariants(4);
    109     sink.assertBytes(new byte[] { 1, 2, 3, 4 });
    110   }
    111 
    112   public void testDouble() {
    113     Sink sink = new Sink(8);
    114     sink.putDouble(Double.longBitsToDouble(0x0807060504030201L));
    115     sink.hash();
    116     sink.assertInvariants(8);
    117     sink.assertBytes(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
    118   }
    119 
    120   public void testCorrectExceptions() {
    121     Sink sink = new Sink(4);
    122     try {
    123       sink.putBytes(new byte[8], -1, 4);
    124       fail();
    125     } catch (IndexOutOfBoundsException ok) {}
    126     try {
    127       sink.putBytes(new byte[8], 0, 16);
    128       fail();
    129     } catch (IndexOutOfBoundsException ok) {}
    130     try {
    131       sink.putBytes(new byte[8], 0, -1);
    132       fail();
    133     } catch (IndexOutOfBoundsException ok) {}
    134   }
    135 
    136   /**
    137    * This test creates a long random sequence of inputs, then a lot of differently configured
    138    * sinks process it; all should produce the same answer, the only difference should be the
    139    * number of process()/processRemaining() invocations, due to alignment.
    140    */
    141   public void testExhaustive() throws Exception {
    142     Random random = new Random(0); // will iteratively make more debuggable, each time it breaks
    143     for (int totalInsertions = 0; totalInsertions < 200; totalInsertions++) {
    144 
    145       List<Sink> sinks = Lists.newArrayList();
    146       for (int chunkSize = 4; chunkSize <= 32; chunkSize++) {
    147         for (int bufferSize = chunkSize; bufferSize <= chunkSize * 4; bufferSize += chunkSize) {
    148           // yes, that's a lot of sinks!
    149           sinks.add(new Sink(chunkSize, bufferSize));
    150           // For convenience, testing only with big endianness, to match DataOutputStream.
    151           // I regard highly unlikely that both the little endianness tests above and this one
    152           // passes, and there is still a little endianness bug lurking around.
    153         }
    154       }
    155 
    156       Control control = new Control();
    157       Hasher controlSink = control.newHasher(1024);
    158 
    159       Iterable<Hasher> sinksAndControl =
    160           Iterables.concat(sinks, Collections.singleton(controlSink));
    161       for (int insertion = 0; insertion < totalInsertions; insertion++) {
    162         RandomHasherAction.pickAtRandom(random).performAction(random, sinksAndControl);
    163       }
    164       // We need to ensure that at least 4 bytes have been put into the hasher or else
    165       // Hasher#hash will throw an ISE.
    166       int intToPut = random.nextInt();
    167       for (Hasher hasher : sinksAndControl) {
    168         hasher.putInt(intToPut);
    169       }
    170       for (Sink sink : sinks) {
    171         sink.hash();
    172       }
    173 
    174       byte[] expected = controlSink.hash().asBytes();
    175       for (Sink sink : sinks) {
    176         sink.assertInvariants(expected.length);
    177         sink.assertBytes(expected);
    178       }
    179     }
    180   }
    181 
    182   private static class Sink extends AbstractStreamingHasher {
    183     final int chunkSize;
    184     final int bufferSize;
    185     final ByteArrayOutputStream out = new ByteArrayOutputStream();
    186 
    187     int processCalled = 0;
    188     boolean remainingCalled = false;
    189 
    190     Sink(int chunkSize, int bufferSize) {
    191       super(chunkSize, bufferSize);
    192       this.chunkSize = chunkSize;
    193       this.bufferSize = bufferSize;
    194     }
    195 
    196     Sink(int chunkSize) {
    197       super(chunkSize);
    198       this.chunkSize = chunkSize;
    199       this.bufferSize = chunkSize;
    200     }
    201 
    202     @Override HashCode makeHash() {
    203       return HashCode.fromBytes(out.toByteArray());
    204     }
    205 
    206     @Override protected void process(ByteBuffer bb) {
    207       processCalled++;
    208       assertEquals(ByteOrder.LITTLE_ENDIAN, bb.order());
    209       assertTrue(bb.remaining() >= chunkSize);
    210       for (int i = 0; i < chunkSize; i++) {
    211         out.write(bb.get());
    212       }
    213     }
    214 
    215     @Override protected void processRemaining(ByteBuffer bb) {
    216       assertFalse(remainingCalled);
    217       remainingCalled = true;
    218       assertEquals(ByteOrder.LITTLE_ENDIAN, bb.order());
    219       assertTrue(bb.remaining() > 0);
    220       assertTrue(bb.remaining() < bufferSize);
    221       int before = processCalled;
    222       super.processRemaining(bb);
    223       int after = processCalled;
    224       assertEquals(before + 1, after); // default implementation pads and calls process()
    225       processCalled--; // don't count the tail invocation (makes tests a bit more understandable)
    226     }
    227 
    228     // ensures that the number of invocations looks sane
    229     void assertInvariants(int expectedBytes) {
    230       // we should have seen as many bytes as the next multiple of chunk after expectedBytes - 1
    231       assertEquals(out.toByteArray().length, ceilToMultiple(expectedBytes, chunkSize));
    232       assertEquals(expectedBytes / chunkSize, processCalled);
    233       assertEquals(expectedBytes % chunkSize != 0, remainingCalled);
    234     }
    235 
    236     // returns the minimum x such as x >= a && (x % b) == 0
    237     private static int ceilToMultiple(int a, int b) {
    238       int remainder = a % b;
    239       return remainder == 0 ? a : a + b - remainder;
    240     }
    241 
    242     void assertBytes(byte[] expected) {
    243       byte[] got = out.toByteArray();
    244       for (int i = 0; i < expected.length; i++) {
    245         assertEquals(expected[i], got[i]);
    246       }
    247     }
    248   }
    249 
    250   // Assumes that AbstractNonStreamingHashFunction works properly (must be tested elsewhere!)
    251   private static class Control extends AbstractNonStreamingHashFunction {
    252     @Override
    253     public HashCode hashBytes(byte[] input) {
    254       return HashCode.fromBytes(input);
    255     }
    256 
    257     @Override
    258     public HashCode hashBytes(byte[] input, int off, int len) {
    259       return hashBytes(Arrays.copyOfRange(input, off, off + len));
    260     }
    261 
    262     @Override
    263     public int bits() {
    264       throw new UnsupportedOperationException();
    265     }
    266 
    267     @Override
    268     public HashCode hashString(CharSequence input) {
    269       throw new UnsupportedOperationException();
    270     }
    271 
    272     @Override
    273     public HashCode hashString(CharSequence input, Charset charset) {
    274       throw new UnsupportedOperationException();
    275     }
    276 
    277     @Override
    278     public HashCode hashLong(long input) {
    279       throw new UnsupportedOperationException();
    280     }
    281 
    282     @Override
    283     public HashCode hashInt(int input) {
    284       throw new UnsupportedOperationException();
    285     }
    286   }
    287 }
    288