Open-source some more serialization codecs, and create a PrecomputedValue codec. Since PrecomputedValues can contain any value, give them access to an ObjectCodecs instance so we don't have to have a whitelist inside PrecomputedValueCodec.

PiperOrigin-RevId: 168624137
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValueCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValueCodec.java
new file mode 100644
index 0000000..cae35b4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValueCodec.java
@@ -0,0 +1,64 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe;
+
+import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
+import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecs;
+import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.IOException;
+import java.util.function.Supplier;
+
+/**
+ * {@link ObjectCodec} for {@link PrecomputedValue} objects. Because {@link PrecomputedValue}
+ * objects can theoretically contain any kind of value object as their value, an {@link
+ * ObjectCodecs} instance must be accessible to this codec during serialization/deserialization, so
+ * that it can handle the arbitrary objects it's given.
+ */
+public class PrecomputedValueCodec implements ObjectCodec<PrecomputedValue> {
+  private final Supplier<ObjectCodecs> objectCodecsSupplier;
+
+  public PrecomputedValueCodec(Supplier<ObjectCodecs> objectCodecsSupplier) {
+    this.objectCodecsSupplier = objectCodecsSupplier;
+  }
+
+  @Override
+  public Class<PrecomputedValue> getEncodedClass() {
+    return PrecomputedValue.class;
+  }
+
+  @Override
+  public void serialize(PrecomputedValue obj, CodedOutputStream codedOut)
+      throws SerializationException, IOException {
+    ObjectCodecs objectCodecs = objectCodecsSupplier.get();
+    Object val = obj.get();
+    Preconditions.checkState(!(val instanceof PrecomputedValue), "recursive precomputed: %s", obj);
+    // TODO(janakr): this assumes the classifier is the class of the object. This should be enforced
+    // by the ObjectCodecs instance.
+    String classifier = val.getClass().getName();
+    codedOut.writeStringNoTag(classifier);
+    objectCodecs.serialize(classifier, val, codedOut);
+  }
+
+  @Override
+  public PrecomputedValue deserialize(CodedInputStream codedIn)
+      throws SerializationException, IOException {
+    ObjectCodecs objectCodecs = objectCodecsSupplier.get();
+    Object val = objectCodecs.deserialize(codedIn.readBytes(), codedIn);
+    return new PrecomputedValue(val);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
index 9f58769..b747a56 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
@@ -9,6 +9,7 @@
     name = "serialization",
     srcs = glob(["*.java"]),
     deps = [
+        "//src/main/java/com/google/devtools/build/lib:preconditions",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects",
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodec.java
new file mode 100644
index 0000000..f3ce577
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodec.java
@@ -0,0 +1,65 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.ByteBuffer;
+
+/** Naive ObjectCodec using Java native Serialization. Not performant, but a good fallback */
+class JavaSerializableCodec implements ObjectCodec<Object> {
+
+  @Override
+  public Class<Object> getEncodedClass() {
+    return Object.class;
+  }
+
+  @Override
+  public void serialize(Object obj, CodedOutputStream codedOut)
+      throws SerializationException, IOException {
+    ByteString.Output out = ByteString.newOutput();
+    ObjectOutputStream objOut = new ObjectOutputStream(out);
+    try {
+      objOut.writeObject(obj);
+    } catch (NotSerializableException e) {
+      throw new SerializationException.NoCodecException("Object " + obj + " not serializable", e);
+    } catch (NotSerializableRuntimeException e) {
+      // Values that inherit from Serializable but actually aren't serializable.
+      throw new SerializationException.NoCodecException("Object " + obj + " not serializable", e);
+    }
+    codedOut.writeBytesNoTag(out.toByteString());
+  }
+
+  @Override
+  public Object deserialize(CodedInputStream codedIn) throws SerializationException, IOException {
+    try {
+      // Get the ByteBuffer as it is potentially a view of the underlying bytes (not a copy), which
+      // is more efficient.
+      ByteBuffer buffer = codedIn.readByteBuffer();
+      ObjectInputStream objIn =
+          new ObjectInputStream(
+              new ByteArrayInputStream(buffer.array(), buffer.arrayOffset(), buffer.remaining()));
+      return objIn.readObject();
+    } catch (ClassNotFoundException e) {
+      throw new SerializationException("Java deserialization failed", e);
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java
new file mode 100644
index 0000000..5778cdb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java
@@ -0,0 +1,272 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Wrapper for the minutiae of serializing and deserializing objects using {@link ObjectCodec}s,
+ * serving as a layer between the streaming-oriented {@link ObjectCodec} interface and users.
+ * Handles the mapping and selection of custom serialization implementations, falling back on less
+ * performant java serialization by default when no better option is available and it is allowed by
+ * the configuration.
+ *
+ * <p>To use, create a {@link ObjectCodecs.Builder} and add custom classifier to {@link ObjectCodec}
+ * mappings using {@link ObjectCodecs.Builder#add}. The provided mappings used to determine
+ * serialization/deserialization logic. For example:
+ *
+ * <pre>{@code
+ * // Create an instance for which anything identified as "foo" will use FooCodec.
+ * ObjectCodecs objectCodecs = ObjectCodecs.newBuilder()
+ *     .add("foo", new FooCodec())
+ *     .build();
+ *
+ * // This will use the custom supplied FooCodec to serialize obj:
+ * ByteString serialized = objectCodecs.serialize("foo", obj);
+ * Object deserialized = objectCodecs.deserialize(ByteString.copyFromUtf8("foo"), serialized);
+ *
+ * // This will use default java object serialization to serialize obj:
+ * ByteString serialized = objectCodecs.serialize("bar", obj);
+ * Object deserialized = objectCodecs.deserialize(ByteString.copyFromUtf8("bar"), serialized);
+ * }</pre>
+ *
+ * <p>Classifiers will typically be class names or SkyFunction names.
+ */
+public class ObjectCodecs {
+
+  private static final ObjectCodec<Object> DEFAULT_CODEC = new JavaSerializableCodec();
+
+  /** Create new ObjectCodecs.Builder, the preferred instantiation method. */
+  // TODO(janakr,michajlo): Specialize builders into ones keyed by class (even if the class isn't
+  // the one specified by the codec) and ones keyed by string, and expose a getClassifier() method
+  // for ObjectCodecs keyed by class.
+  public static ObjectCodecs.Builder newBuilder() {
+    return new Builder();
+  }
+
+  private final Map<String, ObjectCodec<?>> stringMappedCodecs;
+  private final Map<ByteString, ObjectCodec<?>> byteStringMappedCodecs;
+  private final boolean allowDefaultCodec;
+
+  private ObjectCodecs(Map<String, ObjectCodec<?>> codecs, boolean allowDefaultCodec) {
+    this.stringMappedCodecs = codecs;
+    this.byteStringMappedCodecs = makeByteStringMappedCodecs(codecs);
+    this.allowDefaultCodec = allowDefaultCodec;
+  }
+
+  /**
+   * Serialize {@code subject}, using the serialization strategy determined by {@code classifier},
+   * returning a {@link ByteString} containing the serialized representation.
+   */
+  public ByteString serialize(String classifier, Object subject) throws SerializationException {
+    ByteString.Output resultOut = ByteString.newOutput();
+    CodedOutputStream codedOut = CodedOutputStream.newInstance(resultOut);
+    ObjectCodec<?> codec = getCodec(classifier);
+    try {
+      doSerialize(classifier, codec, subject, codedOut);
+      codedOut.flush();
+      return resultOut.toByteString();
+    } catch (IOException e) {
+      throw new SerializationException(
+          "Failed to serialize " + subject + " using " + codec + " for " + classifier, e);
+    }
+  }
+
+  /**
+   * Similar to {@link #serialize(String, Object)}, except allows the caller to specify a {@link
+   * CodedOutputStream} to serialize {@code subject} to. Has less object overhead than {@link
+   * #serialize(String, Object)} and as such is preferrable when serializing objects in bulk.
+   *
+   * <p>{@code codedOut} is not flushed by this method.
+   */
+  public void serialize(String classifier, Object subject, CodedOutputStream codedOut)
+      throws SerializationException {
+    ObjectCodec<?> codec = getCodec(classifier);
+    try {
+      doSerialize(classifier, codec, subject, codedOut);
+    } catch (IOException e) {
+      throw new SerializationException(
+          "Failed to serialize " + subject + " using " + codec + " for " + classifier, e);
+    }
+  }
+
+  /**
+   * Deserialize {@code data} using the serialization strategy determined by {@code classifier}.
+   * {@code classifier} should be the utf-8 encoded {@link ByteString} representation of the {@link
+   * String} classifier used to serialize {@code data}. This is preferred since callers typically
+   * have parsed {@code classifier} from a protocol buffer, for which {@link ByteString}s are
+   * cheaper to use.
+   */
+  public Object deserialize(ByteString classifier, ByteString data) throws SerializationException {
+    return deserialize(classifier, data.newCodedInput());
+  }
+
+  /**
+   * Similar to {@link #deserialize(ByteString, ByteString)}, except allows the caller to specify a
+   * {@link CodedInputStream} to deserialize data from. This is useful for decoding objects
+   * serialized in bulk by {@link #serialize(String, Object, CodedOutputStream)}.
+   */
+  public Object deserialize(ByteString classifier, CodedInputStream codedIn)
+      throws SerializationException {
+    ObjectCodec<?> codec = getCodec(classifier);
+    // If safe, this will allow CodedInputStream to return a direct view of the underlying bytes
+    // in some situations, bypassing a copy.
+    codedIn.enableAliasing(true);
+    try {
+      Object result = codec.deserialize(codedIn);
+      if (result == null) {
+        throw new NullPointerException(
+            "ObjectCodec " + codec + " for " + classifier.toStringUtf8() + " returned null");
+      }
+      return result;
+    } catch (IOException e) {
+      throw new SerializationException(
+          "Failed to deserialize data using " + codec + " for " + classifier.toStringUtf8(), e);
+    }
+  }
+
+  private ObjectCodec<?> getCodec(String classifier)
+      throws SerializationException.NoCodecException {
+    ObjectCodec<?> result = stringMappedCodecs.get(classifier);
+    if (result != null) {
+      return result;
+    } else if (allowDefaultCodec) {
+      return DEFAULT_CODEC;
+    } else {
+      throw new SerializationException.NoCodecException(
+          "No codec available for " + classifier + " and default fallback disabled");
+    }
+  }
+
+  private ObjectCodec<?> getCodec(ByteString classifier) throws SerializationException {
+    ObjectCodec<?> result = byteStringMappedCodecs.get(classifier);
+    if (result != null) {
+      return result;
+    } else if (allowDefaultCodec) {
+      return DEFAULT_CODEC;
+    } else {
+      throw new SerializationException(
+          "No codec available for " + classifier.toStringUtf8() + " and default fallback disabled");
+    }
+  }
+
+  private static <T> void doSerialize(
+      String classifier, ObjectCodec<T> codec, Object subject, CodedOutputStream codedOut)
+      throws SerializationException, IOException {
+    try {
+      codec.serialize(codec.getEncodedClass().cast(subject), codedOut);
+    } catch (ClassCastException e) {
+      throw new SerializationException(
+          "Codec "
+              + codec
+              + " for "
+              + classifier
+              + " is incompatible with "
+              + subject
+              + " (of type "
+              + subject.getClass().getName()
+              + ")",
+          e);
+    }
+  }
+
+  /** Builder for {@link ObjectCodecs}. */
+  static class Builder {
+    private final ImmutableMap.Builder<String, ObjectCodec<?>> codecsBuilder =
+        ImmutableMap.builder();
+    private boolean allowDefaultCodec = true;
+
+    private Builder() {}
+
+    /** Add custom serialization strategy ({@code codec}) for {@code classifier}. */
+    public Builder add(String classifier, ObjectCodec<?> codec) {
+      codecsBuilder.put(classifier, codec);
+      return this;
+    }
+
+    /** Set whether or not we allow fallback to the default codec, java serialization. */
+    public Builder setAllowDefaultCodec(boolean allowDefaultCodec) {
+      this.allowDefaultCodec = allowDefaultCodec;
+      return this;
+    }
+
+    /** Wrap this builder with a {@link ClassKeyedBuilder}. */
+    public ClassKeyedBuilder asClassKeyedBuilder() {
+      return new ClassKeyedBuilder(this);
+    }
+
+    /** Wrap this builder with a {@link SkyFunctionNameKeyedBuilder}. */
+    public SkyFunctionNameKeyedBuilder asSkyFunctionNameKeyedBuilder() {
+      return new SkyFunctionNameKeyedBuilder(this);
+    }
+
+    public ObjectCodecs build() {
+      return new ObjectCodecs(codecsBuilder.build(), allowDefaultCodec);
+    }
+  }
+
+  /** Convenience builder for adding codecs classified by class name. */
+  static class ClassKeyedBuilder {
+    private final Builder underlying;
+
+    private ClassKeyedBuilder(Builder underlying) {
+      this.underlying = underlying;
+    }
+
+    public <T> ClassKeyedBuilder add(Class<T> clazz, ObjectCodec<? extends T> codec) {
+      underlying.add(clazz.getName(), codec);
+      return this;
+    }
+
+    public ObjectCodecs build() {
+      return underlying.build();
+    }
+  }
+
+  /** Convenience builder for adding codecs classified by SkyFunctionName. */
+  static class SkyFunctionNameKeyedBuilder {
+    private final Builder underlying;
+
+    private SkyFunctionNameKeyedBuilder(Builder underlying) {
+      this.underlying = underlying;
+    }
+
+    public SkyFunctionNameKeyedBuilder add(SkyFunctionName skyFuncName, ObjectCodec<?> codec) {
+      underlying.add(skyFuncName.getName(), codec);
+      return this;
+    }
+
+    public ObjectCodecs build() {
+      return underlying.build();
+    }
+  }
+
+  private static Map<ByteString, ObjectCodec<?>> makeByteStringMappedCodecs(
+      Map<String, ObjectCodec<?>> stringMappedCodecs) {
+    ImmutableMap.Builder<ByteString, ObjectCodec<?>> result = ImmutableMap.builder();
+    for (Entry<String, ObjectCodec<?>> entry : stringMappedCodecs.entrySet()) {
+      result.put(ByteString.copyFromUtf8(entry.getKey()), entry.getValue());
+    }
+    return result.build();
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathCodec.java
new file mode 100644
index 0000000..4aca0b0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathCodec.java
@@ -0,0 +1,57 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.IOException;
+
+/** Custom serialization for {@link Path}s. */
+public class PathCodec implements ObjectCodec<Path> {
+
+  private final FileSystem fileSystem;
+  private final PathFragmentCodec pathFragmentCodec;
+
+  /** Create an instance for serializing and deserializing {@link Path}s on {@code fileSystem}. */
+  PathCodec(FileSystem fileSystem) {
+    this.fileSystem = fileSystem;
+    this.pathFragmentCodec = new PathFragmentCodec();
+  }
+
+  @Override
+  public Class<Path> getEncodedClass() {
+    return Path.class;
+  }
+
+  @Override
+  public void serialize(Path path, CodedOutputStream codedOut) throws IOException {
+    Preconditions.checkState(
+        path.getFileSystem() == fileSystem,
+        "Path's FileSystem (%s) did not match the configured FileSystem (%s)",
+        path.getFileSystem(),
+        fileSystem);
+    pathFragmentCodec.serialize(path.asFragment(), codedOut);
+  }
+
+  @Override
+  public Path deserialize(CodedInputStream codedIn) throws IOException {
+    PathFragment pathFragment = pathFragmentCodec.deserialize(codedIn);
+    return fileSystem.getPath(pathFragment);
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathFragmentCodec.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathFragmentCodec.java
index 7438f6d..8cd06f2 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathFragmentCodec.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/PathFragmentCodec.java
@@ -22,7 +22,7 @@
 /** Custom serialization for {@link PathFragment}s. */
 class PathFragmentCodec implements ObjectCodec<PathFragment> {
 
-  private final ObjectCodec<String> stringCodec = new FastStringCodec();
+  private final FastStringCodec stringCodec = new FastStringCodec();
 
   @Override
   public Class<PathFragment> getEncodedClass() {
@@ -30,8 +30,7 @@
   }
 
   @Override
-  public void serialize(PathFragment pathFragment, CodedOutputStream codedOut)
-      throws IOException, SerializationException {
+  public void serialize(PathFragment pathFragment, CodedOutputStream codedOut) throws IOException {
     codedOut.writeInt32NoTag(pathFragment.getDriveLetter());
     codedOut.writeBoolNoTag(pathFragment.isAbsolute());
     codedOut.writeInt32NoTag(pathFragment.segmentCount());
@@ -41,8 +40,7 @@
   }
 
   @Override
-  public PathFragment deserialize(CodedInputStream codedIn)
-      throws IOException, SerializationException {
+  public PathFragment deserialize(CodedInputStream codedIn) throws IOException {
     char driveLetter = (char) codedIn.readInt32();
     boolean isAbsolute = codedIn.readBool();
     int segmentCount = codedIn.readInt32();
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
index 2dd0f92..b59c774 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD
@@ -12,6 +12,7 @@
 
 TEST_BASE_FILES = [
     "AbstractObjectCodecTest.java",
+    "FsUtils.java",
     "TestUtils.java",
 ]
 
@@ -25,6 +26,8 @@
     deps = [
         "//src/main/java/com/google/devtools/build/lib:syntax",
         "//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
+        "//src/main/java/com/google/devtools/build/lib/vfs",
+        "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
         "//third_party:guava",
         "//third_party:jsr305",
         "//third_party:junit4",
@@ -55,6 +58,7 @@
         "//third_party:junit4",
         "//third_party:mockito",
         "//third_party:truth",
+        "//third_party/protobuf:protobuf_java",
     ],
 )
 
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/FsUtils.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/FsUtils.java
new file mode 100644
index 0000000..d38aab0
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/FsUtils.java
@@ -0,0 +1,38 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
+
+/** Common FileSystem related items for serialization tests. */
+class FsUtils {
+
+  static final FileSystem TEST_FILESYSTEM = new InMemoryFileSystem();
+
+  static final RootedPath TEST_ROOT =
+      RootedPath.toRootedPath(
+          TEST_FILESYSTEM.getPath(PathFragment.create("/anywhere/at/all")),
+          PathFragment.create("all/at/anywhere"));
+
+  private FsUtils() {}
+
+  /** Returns path relative to {@link #TEST_ROOT}. */
+  static PathFragment rootPathRelative(String path) {
+    return TEST_ROOT.getRelativePath().getRelative(path);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodecTest.java
new file mode 100644
index 0000000..db200a3
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/JavaSerializableCodecTest.java
@@ -0,0 +1,27 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Basic tests for {@link JavaSerializableCodec}. */
+@RunWith(JUnit4.class)
+public class JavaSerializableCodecTest extends AbstractObjectCodecTest<Object> {
+
+  public JavaSerializableCodecTest() {
+    super(new JavaSerializableCodec(), "strings are serializable");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecsTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecsTest.java
new file mode 100644
index 0000000..3ad0d3c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecsTest.java
@@ -0,0 +1,254 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/** Tests for {@link ObjectCodecs}. */
+@RunWith(JUnit4.class)
+public class ObjectCodecsTest {
+
+  /** Dummy ObjectCodec implementation so we can verify nice type system interaction. */
+  private static class IntegerCodec implements ObjectCodec<Integer> {
+    @Override
+    public Class<Integer> getEncodedClass() {
+      return Integer.class;
+    }
+
+    @Override
+    public void serialize(Integer obj, CodedOutputStream codedOut)
+        throws SerializationException, IOException {
+      codedOut.writeInt32NoTag(obj);
+    }
+
+    @Override
+    public Integer deserialize(CodedInputStream codedIn)
+        throws SerializationException, IOException {
+      return codedIn.readInt32();
+    }
+  }
+
+  private static final String KNOWN_CLASSIFIER = "KNOWN_CLASSIFIER";
+  private static final ByteString KNOWN_CLASSIFIER_BYTES =
+      ByteString.copyFromUtf8("KNOWN_CLASSIFIER");
+
+  private static final String UNKNOWN_CLASSIFIER = "UNKNOWN_CLASSIFIER";
+  private static final ByteString UNKNOWN_CLASSIFIER_BYTES =
+      ByteString.copyFromUtf8("UNKNOWN_CLASSIFIER");
+
+  private ObjectCodec<Integer> spyObjectCodec;
+
+  private ObjectCodecs underTest;
+
+  @Before
+  public final void setup() {
+    spyObjectCodec = spy(new IntegerCodec());
+    this.underTest = ObjectCodecs.newBuilder().add(KNOWN_CLASSIFIER, spyObjectCodec).build();
+  }
+
+  @Test
+  public void testSerializeDeserializeUsesCustomLogicWhenAvailable() throws Exception {
+    Integer original = Integer.valueOf(12345);
+
+    doAnswer(
+            new Answer<Void>() {
+              @Override
+              public Void answer(InvocationOnMock invocation) throws IOException {
+                CodedOutputStream codedOutArg = (CodedOutputStream) invocation.getArguments()[1];
+                codedOutArg.writeInt32NoTag(42);
+                return null;
+              }
+            })
+        .when(spyObjectCodec)
+        .serialize(eq(original), any(CodedOutputStream.class));
+    ArgumentCaptor<CodedInputStream> captor = ArgumentCaptor.forClass(CodedInputStream.class);
+    doReturn(original).when(spyObjectCodec).deserialize(captor.capture());
+
+    ByteString serialized = underTest.serialize(KNOWN_CLASSIFIER, original);
+    Object deserialized = underTest.deserialize(KNOWN_CLASSIFIER_BYTES, serialized);
+    assertThat(deserialized).isEqualTo(original);
+
+    assertThat(captor.getValue().readInt32()).isEqualTo(42);
+  }
+
+  @Test
+  public void testMismatchedArgRaisesException() throws Exception {
+    Long notRight = Long.valueOf(123456789);
+    try {
+      underTest.serialize(KNOWN_CLASSIFIER, notRight);
+      fail("Expected exception");
+    } catch (SerializationException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeDeserializeWhenNocustomLogicAvailable() throws Exception {
+    Integer original = Integer.valueOf(12345);
+
+    ByteString serialized = underTest.serialize(UNKNOWN_CLASSIFIER, original);
+    Object deserialized = underTest.deserialize(UNKNOWN_CLASSIFIER_BYTES, serialized);
+    assertThat(deserialized).isEqualTo(original);
+
+    verify(spyObjectCodec, never()).serialize(any(Integer.class), any(CodedOutputStream.class));
+    verify(spyObjectCodec, never()).deserialize(any(CodedInputStream.class));
+  }
+
+  @Test
+  public void testSerializePropagatesSerializationExceptionFromCustomCodec() throws Exception {
+    Integer original = Integer.valueOf(12345);
+
+    SerializationException staged = new SerializationException("BECAUSE FAIL");
+    doThrow(staged).when(spyObjectCodec).serialize(eq(original), any(CodedOutputStream.class));
+    try {
+      underTest.serialize(KNOWN_CLASSIFIER, original);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e).isSameAs(staged);
+    }
+  }
+
+  @Test
+  public void testSerializePropagatesIOExceptionFromCustomCodecsAsSerializationException()
+      throws Exception {
+    Integer original = Integer.valueOf(12345);
+
+    IOException staged = new IOException("BECAUSE FAIL");
+    doThrow(staged).when(spyObjectCodec).serialize(eq(original), any(CodedOutputStream.class));
+    try {
+      underTest.serialize(KNOWN_CLASSIFIER, original);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e).hasCauseThat().isSameAs(staged);
+    }
+  }
+
+  @Test
+  public void testDeserializePropagatesSerializationExceptionFromCustomCodec() throws Exception {
+    SerializationException staged = new SerializationException("BECAUSE FAIL");
+    doThrow(staged).when(spyObjectCodec).deserialize(any(CodedInputStream.class));
+    try {
+      underTest.deserialize(KNOWN_CLASSIFIER_BYTES, ByteString.EMPTY);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e).isSameAs(staged);
+    }
+  }
+
+  @Test
+  public void testDeserializePropagatesIOExceptionFromCustomCodecAsSerializationException()
+      throws Exception {
+    IOException staged = new IOException("BECAUSE FAIL");
+    doThrow(staged).when(spyObjectCodec).deserialize(any(CodedInputStream.class));
+    try {
+      underTest.deserialize(KNOWN_CLASSIFIER_BYTES, ByteString.EMPTY);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e).hasCauseThat().isSameAs(staged);
+    }
+  }
+
+  @Test
+  public void testSerializePropagatesSerializationExceptionFromDefaultCodec() throws Exception {
+    Object nonSerializable = new Object();
+
+    try {
+      underTest.serialize(UNKNOWN_CLASSIFIER, nonSerializable);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e).hasCauseThat().isInstanceOf(NotSerializableException.class);
+    }
+  }
+
+  @Test
+  public void testDeserializePropagatesSerializationExceptionFromDefaultCodec() throws Exception {
+    ByteString serialized = ByteString.copyFromUtf8("probably not serialized anything");
+
+    try {
+      underTest.deserialize(UNKNOWN_CLASSIFIER_BYTES, serialized);
+      fail("Expected exception");
+    } catch (SerializationException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeFailsWhenNoCustomCodecAndFallbackDisabled() throws Exception {
+    try {
+      ObjectCodecs.newBuilder().setAllowDefaultCodec(false).build().serialize("X", "Y");
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo("No codec available for X and default fallback disabled");
+    }
+  }
+
+  @Test
+  public void testDeserializeFailsWhenNoCustomCodecAndFallbackDisabled() throws Exception {
+    ByteString serialized = ByteString.copyFromUtf8("doesn't matter");
+    try {
+      ObjectCodecs.newBuilder()
+          .setAllowDefaultCodec(false)
+          .build()
+          .deserialize(ByteString.copyFromUtf8("X"), serialized);
+      fail("Expected exception");
+    } catch (SerializationException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo("No codec available for X and default fallback disabled");
+    }
+  }
+
+  @Test
+  public void testSerializeDeserializeInBulk() throws Exception {
+    Integer value1 = 12345;
+    Integer value2 = 67890;
+    Integer value3 = 42;
+
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    CodedOutputStream codedOut = CodedOutputStream.newInstance(bytesOut);
+    underTest.serialize(KNOWN_CLASSIFIER, value1, codedOut);
+    underTest.serialize(KNOWN_CLASSIFIER, value2, codedOut);
+    underTest.serialize(KNOWN_CLASSIFIER, value3, codedOut);
+    codedOut.flush();
+
+    CodedInputStream codedIn = CodedInputStream.newInstance(bytesOut.toByteArray());
+    assertThat(underTest.deserialize(KNOWN_CLASSIFIER_BYTES, codedIn)).isEqualTo(value1);
+    assertThat(underTest.deserialize(KNOWN_CLASSIFIER_BYTES, codedIn)).isEqualTo(value2);
+    assertThat(underTest.deserialize(KNOWN_CLASSIFIER_BYTES, codedIn)).isEqualTo(value3);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PathCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PathCodecTest.java
new file mode 100644
index 0000000..3b64936
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PathCodecTest.java
@@ -0,0 +1,32 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.devtools.build.lib.vfs.Path;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PathCodec}. */
+@RunWith(JUnit4.class)
+public class PathCodecTest extends AbstractObjectCodecTest<Path> {
+
+  public PathCodecTest() {
+    super(
+        new PathCodec(FsUtils.TEST_FILESYSTEM),
+        FsUtils.TEST_FILESYSTEM.getPath("/"),
+        FsUtils.TEST_FILESYSTEM.getPath("/some/path"),
+        FsUtils.TEST_FILESYSTEM.getPath("/some/other/path/with/empty/last/fragment/"));
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PrecomputedValueCodecTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PrecomputedValueCodecTest.java
new file mode 100644
index 0000000..48b3d3d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/PrecomputedValueCodecTest.java
@@ -0,0 +1,41 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.skyframe.serialization;
+
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.skyframe.PrecomputedValue;
+import com.google.devtools.build.lib.skyframe.PrecomputedValueCodec;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/** Tests for {@link PrecomputedValueCodec}. */
+public class PrecomputedValueCodecTest extends AbstractObjectCodecTest<PrecomputedValue> {
+  public PrecomputedValueCodecTest() {
+    super(
+        new PrecomputedValueCodec(
+            () ->
+                ObjectCodecs.newBuilder()
+                    .asClassKeyedBuilder()
+                    // Note no PathFragmentCodec.
+                    .add(String.class, new FastStringCodec())
+                    .add(Label.class, LabelCodec.INSTANCE)
+                    .build()),
+        new PrecomputedValue(PathFragment.create("java serializable 1")),
+        new PrecomputedValue(PathFragment.create("java serializable 2")),
+        new PrecomputedValue("first string"),
+        new PrecomputedValue("second string"),
+        new PrecomputedValue(Label.parseAbsoluteUnchecked("//foo:bar")),
+        new PrecomputedValue(Label.parseAbsoluteUnchecked("//foo:baz")));
+  }
+}