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',
+ ],
+)