1// Copyright 2011 Google Inc. All Rights Reserved.
2
3package com.google.common.hash;
4
5import com.google.common.collect.Iterables;
6import com.google.common.collect.Lists;
7import com.google.common.hash.AbstractStreamingHashFunction.AbstractStreamingHasher;
8import com.google.common.hash.HashTestUtils.RandomHasherAction;
9
10import junit.framework.TestCase;
11
12import java.io.ByteArrayOutputStream;
13import java.nio.ByteBuffer;
14import java.nio.ByteOrder;
15import java.nio.charset.Charset;
16import java.util.Arrays;
17import java.util.Collections;
18import java.util.List;
19import java.util.Random;
20
21/**
22 * Tests for AbstractHashSink.
23 *
24 * @author andreou@google.com (Dimitris Andreou)
25 */
26public class AbstractStreamingHasherTest extends TestCase {
27  /** Test we get the HashCode that is created by the sink. Later we ignore the result */
28  public void testSanity() {
29    Sink sink = new Sink(4);
30    assertEquals(0xDeadBeef, sink.makeHash().asInt());
31  }
32
33  public void testBytes() {
34    Sink sink = new Sink(4); // byte order insignificant here
35    byte[] expected = { 1, 2, 3, 4, 5, 6, 7, 8 };
36    sink.putByte((byte) 1);
37    sink.putBytes(new byte[] { 2, 3, 4, 5, 6 });
38    sink.putByte((byte) 7);
39    sink.putBytes(new byte[] { });
40    sink.putBytes(new byte[] { 8 });
41    sink.hash();
42    sink.assertInvariants(8);
43    sink.assertBytes(expected);
44  }
45
46  public void testShort() {
47    Sink sink = new Sink(4);
48    sink.putShort((short) 0x0201);
49    sink.hash();
50    sink.assertInvariants(2);
51    sink.assertBytes(new byte[] { 1, 2, 0, 0 }); // padded with zeros
52  }
53
54  public void testInt() {
55    Sink sink = new Sink(4);
56    sink.putInt(0x04030201);
57    sink.hash();
58    sink.assertInvariants(4);
59    sink.assertBytes(new byte[] { 1, 2, 3, 4 });
60  }
61
62  public void testLong() {
63    Sink sink = new Sink(8);
64    sink.putLong(0x0807060504030201L);
65    sink.hash();
66    sink.assertInvariants(8);
67    sink.assertBytes(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
68  }
69
70  public void testChar() {
71    Sink sink = new Sink(4);
72    sink.putChar((char) 0x0201);
73    sink.hash();
74    sink.assertInvariants(2);
75    sink.assertBytes(new byte[] { 1, 2, 0, 0  }); // padded with zeros
76  }
77
78  public void testFloat() {
79    Sink sink = new Sink(4);
80    sink.putFloat(Float.intBitsToFloat(0x04030201));
81    sink.hash();
82    sink.assertInvariants(4);
83    sink.assertBytes(new byte[] { 1, 2, 3, 4 });
84  }
85
86  public void testDouble() {
87    Sink sink = new Sink(8);
88    sink.putDouble(Double.longBitsToDouble(0x0807060504030201L));
89    sink.hash();
90    sink.assertInvariants(8);
91    sink.assertBytes(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
92  }
93
94  public void testCorrectExceptions() {
95    Sink sink = new Sink(4);
96    try {
97      sink.putBytes(new byte[8], -1, 4);
98      fail();
99    } catch (IndexOutOfBoundsException ok) {}
100    try {
101      sink.putBytes(new byte[8], 0, 16);
102      fail();
103    } catch (IndexOutOfBoundsException ok) {}
104    try {
105      sink.putBytes(new byte[8], 0, -1);
106      fail();
107    } catch (IndexOutOfBoundsException ok) {}
108  }
109
110  /**
111   * This test creates a long random sequence of inputs, then a lot of differently configured
112   * sinks process it; all should produce the same answer, the only difference should be the
113   * number of process()/processRemaining() invocations, due to alignment.
114   */
115  public void testExhaustive() throws Exception {
116    Random random = new Random(0); // will iteratively make more debuggable, each time it breaks
117    for (int totalInsertions = 0; totalInsertions < 200; totalInsertions++) {
118
119      List<Sink> sinks = Lists.newArrayList();
120      for (int chunkSize = 4; chunkSize <= 32; chunkSize++) {
121        for (int bufferSize = chunkSize; bufferSize <= chunkSize * 4; bufferSize += chunkSize) {
122          // yes, that's a lot of sinks!
123          sinks.add(new Sink(chunkSize, bufferSize));
124          // For convenience, testing only with big endianness, to match DataOutputStream.
125          // I regard highly unlikely that both the little endianness tests above and this one
126          // passes, and there is still a little endianness bug lurking around.
127        }
128      }
129
130      Control control = new Control();
131      Hasher controlSink = control.newHasher(1024);
132
133      Iterable<Hasher> sinksAndControl = Iterables.concat(
134          sinks, Collections.singleton(controlSink));
135      for (int insertion = 0; insertion < totalInsertions; insertion++) {
136        RandomHasherAction.pickAtRandom(random).performAction(random, sinksAndControl);
137      }
138      for (Sink sink : sinks) {
139        sink.hash();
140      }
141
142      byte[] expected = controlSink.hash().asBytes();
143      for (Sink sink : sinks) {
144        sink.assertInvariants(expected.length);
145        sink.assertBytes(expected);
146      }
147    }
148  }
149
150  private static class Sink extends AbstractStreamingHasher {
151    final int chunkSize;
152    final int bufferSize;
153    final ByteArrayOutputStream out = new ByteArrayOutputStream();
154
155    int processCalled = 0;
156    boolean remainingCalled = false;
157
158    Sink(int chunkSize, int bufferSize) {
159      super(chunkSize, bufferSize);
160      this.chunkSize = chunkSize;
161      this.bufferSize = bufferSize;
162    }
163
164    Sink(int chunkSize) {
165      super(chunkSize);
166      this.chunkSize = chunkSize;
167      this.bufferSize = chunkSize;
168    }
169
170    @Override HashCode makeHash() {
171      return HashCodes.fromInt(0xDeadBeef);
172    }
173
174    @Override protected void process(ByteBuffer bb) {
175      processCalled++;
176      assertEquals(ByteOrder.LITTLE_ENDIAN, bb.order());
177      assertTrue(bb.remaining() >= chunkSize);
178      for (int i = 0; i < chunkSize; i++) {
179        out.write(bb.get());
180      }
181    }
182
183    @Override protected void processRemaining(ByteBuffer bb) {
184      assertFalse(remainingCalled);
185      remainingCalled = true;
186      assertEquals(ByteOrder.LITTLE_ENDIAN, bb.order());
187      assertTrue(bb.remaining() > 0);
188      assertTrue(bb.remaining() < bufferSize);
189      int before = processCalled;
190      super.processRemaining(bb);
191      int after = processCalled;
192      assertEquals(before + 1, after); // default implementation pads and calls process()
193      processCalled--; // don't count the tail invocation (makes tests a bit more understandable)
194    }
195
196    // ensures that the number of invocations looks sane
197    void assertInvariants(int expectedBytes) {
198      // we should have seen as many bytes as the next multiple of chunk after expectedBytes - 1
199      assertEquals(out.toByteArray().length, ceilToMultiple(expectedBytes, chunkSize));
200      assertEquals(expectedBytes / chunkSize, processCalled);
201      assertEquals(expectedBytes % chunkSize != 0, remainingCalled);
202    }
203
204    // returns the minimum x such as x >= a && (x % b) == 0
205    private static int ceilToMultiple(int a, int b) {
206      int remainder = a % b;
207      return remainder == 0 ? a : a + b - remainder;
208    }
209
210    void assertBytes(byte[] expected) {
211      byte[] got = out.toByteArray();
212      for (int i = 0; i < expected.length; i++) {
213        assertEquals(expected[i], got[i]);
214      }
215    }
216  }
217
218  private static class Control extends AbstractNonStreamingHashFunction {
219    @Override
220    public HashCode hashBytes(byte[] input) {
221      return HashCodes.fromBytes(input);
222    }
223
224    @Override
225    public HashCode hashBytes(byte[] input, int off, int len) {
226      return hashBytes(Arrays.copyOfRange(input, off, off + len));
227    }
228
229    @Override
230    public int bits() {
231      throw new UnsupportedOperationException();
232    }
233
234    @Override
235    public HashCode hashString(CharSequence input) {
236      throw new UnsupportedOperationException();
237    }
238
239    @Override
240    public HashCode hashString(CharSequence input, Charset charset) {
241      throw new UnsupportedOperationException();
242    }
243
244    @Override
245    public HashCode hashLong(long input) {
246      throw new UnsupportedOperationException();
247    }
248  }
249}
250