diff options
author | 2017-09-14 02:58:31 +0200 | |
---|---|---|
committer | 2017-09-14 18:47:10 +0200 | |
commit | 23e152c5dd2e7b9a49c4fbaebfe602f871007e1d (patch) | |
tree | 4e924b4844f25cdc228f3577ecde7067fa5610b6 /src | |
parent | dbffeabdb382a4d97316f34ba5c274b044e144a3 (diff) |
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
Diffstat (limited to 'src')
12 files changed, 858 insertions, 5 deletions
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 0000000000..cae35b468a --- /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 9f58769888..b747a567af 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 @@ java_library( 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 0000000000..f3ce577ccc --- /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 0000000000..5778cdb50d --- /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 0000000000..4aca0b07ed --- /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 7438f6de0b..8cd06f2249 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 @@ import java.io.IOException; /** 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 @@ class PathFragmentCodec implements ObjectCodec<PathFragment> { } @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 @@ class PathFragmentCodec implements ObjectCodec<PathFragment> { } @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 2dd0f9230c..b59c774273 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 @@ filegroup( TEST_BASE_FILES = [ "AbstractObjectCodecTest.java", + "FsUtils.java", "TestUtils.java", ] @@ -25,6 +26,8 @@ java_library( 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 @@ java_library( "//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 0000000000..d38aab03fe --- /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 0000000000..db200a377c --- /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 0000000000..3ad0d3c7b1 --- /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 0000000000..3b6493608e --- /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 0000000000..48b3d3d6d3 --- /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"))); + } +} |