Compute the RuleKey with and without deps simultaneously.

Summary:
It turns out that a `Hasher` created via `Hashing.sha1()` can have
its `hash()` method invoked only once. Therefore, to compute both
RuleKeys simultaneously, this diff introduces an AppendingHasher
that makes it possible to build up the SHA1 digest for a RuleKey
without deps, get its hash, and then update the digest with the deps
info, and get its hash again.

Test Plan:
Created a comprehensive unit test, AppendingHasherTest.
The updates to AbstractCachingBuildRuleTest.java reflect the
changes to RuleKey.
diff --git a/src/com/facebook/buck/rules/AbstractBuildRule.java b/src/com/facebook/buck/rules/AbstractBuildRule.java
index 7cd8e4a..a60a238 100644
--- a/src/com/facebook/buck/rules/AbstractBuildRule.java
+++ b/src/com/facebook/buck/rules/AbstractBuildRule.java
@@ -34,7 +34,7 @@
   private final BuildTarget buildTarget;
   private final ImmutableSortedSet<BuildRule> deps;
   private final ImmutableSet<BuildTargetPattern> visibilityPatterns;
-  @Nullable private volatile RuleKey ruleKey;
+  @Nullable private volatile RuleKey.Builder.RuleKeyPair ruleKeyPair;
 
   protected AbstractBuildRule(BuildRuleParams buildRuleParams) {
     Preconditions.checkNotNull(buildRuleParams);
@@ -149,18 +149,7 @@
    */
   @Override
   public RuleKey getRuleKey() throws IOException {
-    // This uses the "double-checked locking using volatile" pattern:
-    // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.
-    if (ruleKey == null) {
-      synchronized (this) {
-        if (ruleKey == null) {
-          RuleKey.Builder builder = RuleKey.builder(this);
-          appendToRuleKey(builder);
-          ruleKey = builder.build();
-        }
-      }
-    }
-    return ruleKey;
+    return getRuleKeyPair().getTotalRuleKey();
   }
 
   /**
@@ -169,7 +158,22 @@
    */
   @Override
   public RuleKey getRuleKeyWithoutDeps() throws IOException {
-    return appendToRuleKey(RuleKey.builderWithoutDeps(this)).build();
+    return getRuleKeyPair().getRuleKeyWithoutDeps();
+  }
+
+  private RuleKey.Builder.RuleKeyPair getRuleKeyPair() throws IOException {
+    // This uses the "double-checked locking using volatile" pattern:
+    // http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.
+    if (ruleKeyPair == null) {
+      synchronized (this) {
+        if (ruleKeyPair == null) {
+          RuleKey.Builder builder = RuleKey.builder(this);
+          appendToRuleKey(builder);
+          ruleKeyPair = builder.build();
+        }
+      }
+    }
+    return ruleKeyPair;
   }
 
   /**
diff --git a/src/com/facebook/buck/rules/BUCK b/src/com/facebook/buck/rules/BUCK
index 1feee6a..63f5a10 100644
--- a/src/com/facebook/buck/rules/BUCK
+++ b/src/com/facebook/buck/rules/BUCK
@@ -66,6 +66,7 @@
     '//src/com/facebook/buck/util:exceptions',
     '//src/com/facebook/buck/util:io',
     '//src/com/facebook/buck/util:util',
+    '//src/com/facebook/buck/util/hash:hash',
   ],
   visibility = [
     'PUBLIC',
diff --git a/src/com/facebook/buck/rules/InputRule.java b/src/com/facebook/buck/rules/InputRule.java
index 02d2de6..4a2caee 100644
--- a/src/com/facebook/buck/rules/InputRule.java
+++ b/src/com/facebook/buck/rules/InputRule.java
@@ -141,7 +141,7 @@
   @Override
   public RuleKey getRuleKey() throws IOException {
     if (this.ruleKey == null) {
-      ruleKey = RuleKey.builder(this).set("inputFile", inputFile).build();
+      ruleKey = RuleKey.builder(this).set("inputFile", inputFile).build().getTotalRuleKey();
     }
     return ruleKey;
   }
diff --git a/src/com/facebook/buck/rules/RuleKey.java b/src/com/facebook/buck/rules/RuleKey.java
index 580d1a2..8d15a37 100644
--- a/src/com/facebook/buck/rules/RuleKey.java
+++ b/src/com/facebook/buck/rules/RuleKey.java
@@ -16,6 +16,7 @@
 
 package com.facebook.buck.rules;
 
+import com.facebook.buck.util.hash.AppendingHasher;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
@@ -113,37 +114,15 @@
   }
 
   /**
-   * Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s input
-   * arguments.
+   * Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s inputs.
    */
   public static Builder builder(BuildRule rule) throws IOException {
-    return builder(rule, /* includeDeps */ true);
-  }
-
-  /**
-   * Builder for a {@link RuleKey} that is a function of all of a {@link BuildRule}'s input
-   * arguments <em>except</em> for its <code>deps</doe>.
-   */
-  public static Builder builderWithoutDeps(BuildRule rule) throws IOException {
-    return builder(rule, /* includeDeps */ false);
-  }
-
-  private static Builder builder(BuildRule rule, boolean includeDeps) throws IOException {
-    Builder builder = new Builder()
+    Builder builder = new Builder(rule)
         .set("name", rule.getFullyQualifiedName())
 
         // Keyed as "buck.type" rather than "type" in case a build rule has its own "type" argument.
         .set("buck.type", rule.getType().getName());
 
-    if (includeDeps) {
-      builder.setKey("deps");
-      // Note that getDeps() returns an ImmutableSortedSet, so the order will be stable.
-      for (BuildRule buildRule : rule.getDeps()) {
-        builder.setVal(buildRule.getRuleKey());
-      }
-    }
-    builder.separate();
-
     return builder;
   }
 
@@ -158,13 +137,15 @@
 
     private static final Logger logger = Logger.getLogger(Builder.class.getName());
 
+    private final BuildRule rule;
     private final Hasher hasher;
     @Nullable private List<String> logElms;
 
-    private Builder() {
-      hasher = Hashing.sha1().newHasher();
+    private Builder(BuildRule rule) {
+      this.rule = Preconditions.checkNotNull(rule);
+      this.hasher = new AppendingHasher(Hashing.sha1(), /* numHashers */ 2);
       if (logger.isLoggable(Level.INFO)) {
-        logElms = Lists.newArrayList();
+        this.logElms = Lists.newArrayList();
       }
       setBuckVersionUID();
     }
@@ -356,12 +337,41 @@
       return separate();
     }
 
-    public RuleKey build() {
-      RuleKey ruleKey = new RuleKey(hasher.hash());
-      if (logElms != null) {
-        logger.info(String.format("RuleKey %s=%s", ruleKey, Joiner.on("").join(logElms)));
+    public static class RuleKeyPair {
+      private final RuleKey totalRuleKey;
+      private final RuleKey ruleKeyWithoutDeps;
+
+      private RuleKeyPair(RuleKey totalRuleKey, RuleKey ruleKeyWithoutDeps) {
+        this.totalRuleKey = Preconditions.checkNotNull(totalRuleKey);
+        this.ruleKeyWithoutDeps = Preconditions.checkNotNull(ruleKeyWithoutDeps);
       }
-      return ruleKey;
+
+      public RuleKey getTotalRuleKey() {
+        return totalRuleKey;
+      }
+
+      public RuleKey getRuleKeyWithoutDeps() {
+        return ruleKeyWithoutDeps;
+      }
+    }
+
+    public RuleKeyPair build() throws IOException {
+      RuleKey ruleKeyWithoutDeps = new RuleKey(hasher.hash());
+
+      // Now introduce the deps into the RuleKey.
+      setKey("deps");
+      // Note that getDeps() returns an ImmutableSortedSet, so the order will be stable.
+      for (BuildRule buildRule : rule.getDeps()) {
+        setVal(buildRule.getRuleKey());
+      }
+      separate();
+      RuleKey totalRuleKey = new RuleKey(hasher.hash());
+
+      if (logElms != null) {
+        logger.info(String.format("RuleKey %s=%s", ruleKeyWithoutDeps, Joiner.on("").join(logElms)));
+      }
+
+      return new RuleKeyPair(totalRuleKey, ruleKeyWithoutDeps);
     }
   }
 }
diff --git a/src/com/facebook/buck/util/hash/AppendingHasher.java b/src/com/facebook/buck/util/hash/AppendingHasher.java
new file mode 100644
index 0000000..cda3ee8
--- /dev/null
+++ b/src/com/facebook/buck/util/hash/AppendingHasher.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.util.hash;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hasher;
+
+import java.nio.charset.Charset;
+import java.util.LinkedList;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * {@link Hasher} whose {@code put*} calls are forwarded to a sequence of {@link Hasher} objects.
+ * <p>
+ * When {@link #hash()} is invoked, the {@link #hash()} method of the first {@link Hasher} in the
+ * sequence is invoked, and then the {@link Hasher} is removed from the sequence. This makes it
+ * possible to invoke additional {@code put*} methods after {@link #hash()} is invoked, which is not
+ * generally true of most implementations of {@link Hasher}. Example:
+ * <pre>
+ * Hasher appendingHasher = new AppendingHasher(Hashing.sha1(), 2);
+ * appendingHasher.putInt(42);
+ * HashCode hashOf42 = appendingHasher.hash();
+ *
+ * // This call would fail if appendingHasher were created via Hashing.sha1().newHasher().
+ * appendingHasher.putInt(24);
+ * HashCode hashOf42And24 = appendingHasher.hash();
+ * </pre>
+ */
+@NotThreadSafe
+public class AppendingHasher implements Hasher {
+
+  private final LinkedList<Hasher> hashers;
+
+  /**
+   * Creates a new {@link AppendingHasher} backed by a sequence of {@code numHasher}
+   * {@link Hasher}s created from the specified {@link HashFunction}.
+   */
+  public AppendingHasher(HashFunction hashFunction, int numHashers) {
+    Preconditions.checkNotNull(hashFunction);
+    Preconditions.checkArgument(numHashers > 0);
+    LinkedList<Hasher> hashers = Lists.newLinkedList();
+    for (int i = 0; i < numHashers; ++i) {
+      hashers.add(hashFunction.newHasher());
+    }
+    this.hashers = hashers;
+  }
+
+  @Override
+  public Hasher putByte(byte b) {
+    for (Hasher hasher : hashers) {
+      hasher.putByte(b);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putBytes(byte[] bytes) {
+    for (Hasher hasher : hashers) {
+      hasher.putBytes(bytes);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putBytes(byte[] bytes, int off, int len) {
+    for (Hasher hasher : hashers) {
+      hasher.putBytes(bytes, off, len);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putShort(short s) {
+    for (Hasher hasher : hashers) {
+      hasher.putShort(s);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putInt(int i) {
+    for (Hasher hasher : hashers) {
+      hasher.putInt(i);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putLong(long l) {
+    for (Hasher hasher : hashers) {
+      hasher.putLong(l);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putFloat(float f) {
+    for (Hasher hasher : hashers) {
+      hasher.putFloat(f);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putDouble(double d) {
+    for (Hasher hasher : hashers) {
+      hasher.putDouble(d);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putBoolean(boolean b) {
+    for (Hasher hasher : hashers) {
+      hasher.putBoolean(b);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putChar(char c) {
+    for (Hasher hasher : hashers) {
+      hasher.putChar(c);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putUnencodedChars(CharSequence charSequence) {
+    for (Hasher hasher : hashers) {
+      hasher.putUnencodedChars(charSequence);
+    }
+    return this;
+  }
+
+  @Override
+  @Deprecated
+  public Hasher putString(CharSequence charSequence) {
+    for (Hasher hasher : hashers) {
+      hasher.putString(charSequence);
+    }
+    return this;
+  }
+
+  @Override
+  public Hasher putString(CharSequence charSequence, Charset charset) {
+    for (Hasher hasher : hashers) {
+      hasher.putString(charSequence, charset);
+    }
+    return this;
+  }
+
+  @Override
+  public <T> Hasher putObject(T instance, Funnel<? super T> funnel) {
+    for (Hasher hasher : hashers) {
+      hasher.putObject(instance, funnel);
+    }
+    return this;
+  }
+
+  @Override
+  public HashCode hash() {
+    return hashers.removeFirst().hash();
+  }
+}
diff --git a/src/com/facebook/buck/util/hash/BUCK b/src/com/facebook/buck/util/hash/BUCK
new file mode 100644
index 0000000..2c5da50
--- /dev/null
+++ b/src/com/facebook/buck/util/hash/BUCK
@@ -0,0 +1,11 @@
+java_library(
+  name = 'hash',
+  srcs = glob(['*.java']),
+  deps = [
+    '//lib:guava',
+    '//lib:jsr305',
+  ],
+  visibility = [
+    'PUBLIC',
+  ],
+)
diff --git a/test/com/facebook/buck/rules/AbstractCachingBuildRuleTest.java b/test/com/facebook/buck/rules/AbstractCachingBuildRuleTest.java
index 8aadd8f..218ca4c 100644
--- a/test/com/facebook/buck/rules/AbstractCachingBuildRuleTest.java
+++ b/test/com/facebook/buck/rules/AbstractCachingBuildRuleTest.java
@@ -147,16 +147,16 @@
         .putByte(RuleKey.Builder.SEPARATOR)
 
         .putByte(RuleKey.Builder.SEPARATOR)
-        .putBytes("deps".getBytes())
+        .putBytes("buck.inputs".getBytes())
         .putByte(RuleKey.Builder.SEPARATOR)
-        .putBytes("19d2558a6bd3a34fb3f95412de9da27ed32fe208".getBytes())
+        .putBytes("ae8c0f860a0ecad94ecede79b69460434eddbfbc".getBytes())
         .putByte(RuleKey.Builder.SEPARATOR)
         .putByte(RuleKey.Builder.SEPARATOR)
 
         .putByte(RuleKey.Builder.SEPARATOR)
-        .putBytes("buck.inputs".getBytes())
+        .putBytes("deps".getBytes())
         .putByte(RuleKey.Builder.SEPARATOR)
-        .putBytes("ae8c0f860a0ecad94ecede79b69460434eddbfbc".getBytes())
+        .putBytes("19d2558a6bd3a34fb3f95412de9da27ed32fe208".getBytes())
         .putByte(RuleKey.Builder.SEPARATOR)
         .putByte(RuleKey.Builder.SEPARATOR)
 
diff --git a/test/com/facebook/buck/rules/DirArtifactCacheTest.java b/test/com/facebook/buck/rules/DirArtifactCacheTest.java
index cf6d755..2fff62f 100644
--- a/test/com/facebook/buck/rules/DirArtifactCacheTest.java
+++ b/test/com/facebook/buck/rules/DirArtifactCacheTest.java
@@ -49,7 +49,7 @@
 
     Files.write("x", fileX, Charsets.UTF_8);
     InputRule inputRuleX = new InputRuleForTest(fileX);
-    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build();
+    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build().getTotalRuleKey();
 
     assertEquals(CacheResult.MISS, dirArtifactCache.fetch(ruleKeyX, fileX));
   }
@@ -63,7 +63,7 @@
 
     Files.write("x", fileX, Charsets.UTF_8);
     InputRule inputRuleX = new InputRuleForTest(fileX);
-    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build();
+    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build().getTotalRuleKey();
 
     dirArtifactCache.store(ruleKeyX, fileX);
 
@@ -86,7 +86,7 @@
 
     Files.write("x", fileX, Charsets.UTF_8);
     InputRule inputRuleX = new InputRuleForTest(fileX);
-    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build();
+    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build().getTotalRuleKey();
 
     dirArtifactCache.store(ruleKeyX, fileX);
     dirArtifactCache.store(ruleKeyX, fileX); // Overwrite.
@@ -115,9 +115,9 @@
     assertFalse(inputRuleX.equals(inputRuleZ));
     assertFalse(inputRuleY.equals(inputRuleZ));
 
-    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build();
-    RuleKey ruleKeyY = RuleKey.builder(inputRuleY).build();
-    RuleKey ruleKeyZ = RuleKey.builder(inputRuleZ).build();
+    RuleKey ruleKeyX = RuleKey.builder(inputRuleX).build().getTotalRuleKey();
+    RuleKey ruleKeyY = RuleKey.builder(inputRuleY).build().getTotalRuleKey();
+    RuleKey ruleKeyZ = RuleKey.builder(inputRuleZ).build().getTotalRuleKey();
 
     assertEquals(CacheResult.MISS, dirArtifactCache.fetch(ruleKeyX, fileX));
     assertEquals(CacheResult.MISS, dirArtifactCache.fetch(ruleKeyY, fileY));
diff --git a/test/com/facebook/buck/util/concurrent/BUCK b/test/com/facebook/buck/util/concurrent/BUCK
index f1e377f..caa975c 100644
--- a/test/com/facebook/buck/util/concurrent/BUCK
+++ b/test/com/facebook/buck/util/concurrent/BUCK
@@ -1,6 +1,9 @@
 java_test(
   name = 'concurrent',
   srcs = glob(['*.java']),
+  source_under_test = [
+    '//src/com/facebook/buck/util/concurrent:concurrent',
+  ],
   deps = [
     '//lib:guava',
     '//lib:junit',
diff --git a/test/com/facebook/buck/util/hash/AppendingHasherTest.java b/test/com/facebook/buck/util/hash/AppendingHasherTest.java
new file mode 100644
index 0000000..46e5521
--- /dev/null
+++ b/test/com/facebook/buck/util/hash/AppendingHasherTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.util.hash;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.common.hash.PrimitiveSink;
+
+import org.junit.Test;
+
+public class AppendingHasherTest {
+
+  @Test
+  public void testAppendingHasher() {
+    HashFunction sha1 = Hashing.sha1();
+    AppendingHasher appendingHasher = new AppendingHasher(sha1, /* numHashers */ 3);
+
+    Hasher hasher1 = sha1.newHasher();
+    Hasher hasher2 = sha1.newHasher();
+    Hasher hasher3 = sha1.newHasher();
+
+    // putDouble(Math.E) to all hashers.
+    appendingHasher.putDouble(Math.E);
+    hasher1.putDouble(Math.E);
+    hasher2.putDouble(Math.E);
+    hasher3.putDouble(Math.E);
+
+    // putFloat(3.14f) to all hashers.
+    appendingHasher.putFloat(3.14f);
+    hasher1.putFloat(3.14f);
+    hasher2.putFloat(3.14f);
+    hasher3.putFloat(3.14f);
+
+    // putBytes(bytes, 2, 7) to all hashers.
+    byte[] bytes = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+    appendingHasher.putBytes(bytes, 2, 7);
+    HashCode firstHash = appendingHasher.hash();
+    hasher1.putBytes(bytes, 2, 7);
+    hasher2.putBytes(bytes, 2, 7);
+    hasher3.putBytes(bytes, 2, 7);
+
+    // putLong(8_000_000_000L) to all but the first hasher.
+    appendingHasher.putLong(8_000_000_000L);
+    HashCode secondHash = appendingHasher.hash();
+    hasher2.putLong(8_000_000_000L);
+    hasher3.putLong(8_000_000_000L);
+
+    // putUnencodedChars("hello") to all but the first and second hasher.
+    appendingHasher.putUnencodedChars("hello");
+    HashCode thirdHash = appendingHasher.hash();
+    hasher3.putUnencodedChars("hello");
+
+    assertEquals(hasher1.hash(), firstHash);
+    assertEquals(hasher2.hash(), secondHash);
+    assertEquals(hasher3.hash(), thirdHash);
+  }
+
+  @Test
+  @SuppressWarnings("deprecation")
+  public void testAllPutMethods() {
+    HashFunction md5 = Hashing.md5();
+    Hasher ordinaryHasher = md5.newHasher();
+    AppendingHasher appendingHasher = new AppendingHasher(md5, 1 /* numHashers */);
+
+    Iterable<Hasher> hashers = ImmutableList.of(ordinaryHasher, appendingHasher);
+    for (Hasher hasher : hashers) {
+      assertSame(hasher, hasher.putByte((byte)42));
+      byte[] bytes = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+      assertSame(hasher, hasher.putBytes(bytes, 2, 7));
+      assertSame(hasher, hasher.putShort((short)300));
+      assertSame(hasher, hasher.putInt(65101));
+      assertSame(hasher, hasher.putLong(8_000_000_000L));
+      assertSame(hasher, hasher.putFloat(3.14f));
+      assertSame(hasher, hasher.putDouble(Math.E));
+      assertSame(hasher, hasher.putBoolean(true));
+      assertSame(hasher, hasher.putChar('\n'));
+      assertSame(hasher, hasher.putUnencodedChars("I like unit tests."));
+      assertSame(hasher, hasher.putString("I know: this method is deprecated."));
+      assertSame(hasher, hasher.putString("abc", Charsets.US_ASCII));
+      assertSame(hasher, hasher.putObject(this.getClass(), TestFunnel.instance));
+    }
+
+    assertEquals(ordinaryHasher.hash(), appendingHasher.hash());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testNonPositiveNumHashersIsDisallowed() {
+    new AppendingHasher(Hashing.adler32(), 0 /* numHashers */);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testNullHashFunctionIsDisallowed() {
+    new AppendingHasher(/* hashFunction */ null, 10 /* numHashers */);
+  }
+
+  @SuppressWarnings("serial")
+  private static class TestFunnel implements Funnel<Class<?>> {
+
+    private static final Funnel<Class<?>> instance = new TestFunnel();
+
+    private TestFunnel() {}
+
+    @Override
+    public void funnel(Class<?> from, PrimitiveSink into) {
+      into.putUnencodedChars(from.getName());
+    }
+  }
+}
diff --git a/test/com/facebook/buck/util/hash/BUCK b/test/com/facebook/buck/util/hash/BUCK
new file mode 100644
index 0000000..a905780
--- /dev/null
+++ b/test/com/facebook/buck/util/hash/BUCK
@@ -0,0 +1,12 @@
+java_test(
+  name = 'hash',
+  srcs = glob(['*.java']),
+  source_under_test = [
+    '//src/com/facebook/buck/util/hash:hash',
+  ],
+  deps = [
+    '//lib:guava',
+    '//lib:junit',
+    '//src/com/facebook/buck/util/hash:hash',
+  ],
+)