blob: 0022d663ebe40592910c4f1cb38672c367f42c46 [file] [log] [blame]
// 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.testutils;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.skyframe.serialization.AutoRegistry;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecRegistry;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecs;
import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
import com.google.protobuf.ByteString;
import java.util.ArrayList;
import java.util.Random;
/**
* Utility for testing serialization of given subjects.
*
* <p>Differs from {@link ObjectCodecTester} in that this uses the context to perform serialization
* deserialization instead of a specific codec.
*/
public class SerializationTester {
public static final int DEFAULT_JUNK_INPUTS = 20;
public static final int JUNK_LENGTH_UPPER_BOUND = 20;
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
/** Interface for testing successful deserialization of an object. */
@FunctionalInterface
public interface VerificationFunction<T> {
/**
* Verifies that the original object was sufficiently serialized/deserialized.
*
* <p><i>Must</i> throw an exception on failure.
*/
void verifyDeserialized(T original, T deserialized) throws Exception;
}
private final ImmutableList<?> subjects;
private final ImmutableClassToInstanceMap.Builder<Object> dependenciesBuilder =
ImmutableClassToInstanceMap.builder();
private final ArrayList<ObjectCodec<?>> additionalCodecs = new ArrayList<>();
private boolean memoize;
private boolean allowFutureBlocking;
private ObjectCodecs objectCodecs;
@SuppressWarnings("rawtypes")
private VerificationFunction verificationFunction =
(original, deserialized) -> assertThat(deserialized).isEqualTo(original);
private int repetitions = 1;
public SerializationTester(Object... subjects) {
this(ImmutableList.copyOf(subjects));
}
public SerializationTester(ImmutableList<?> subjects) {
Preconditions.checkArgument(!subjects.isEmpty());
this.subjects = subjects;
}
public <D> SerializationTester addDependency(Class<? super D> type, D dependency) {
dependenciesBuilder.put(type, dependency);
return this;
}
public SerializationTester addDependencies(ClassToInstanceMap<?> dependencies) {
dependenciesBuilder.putAll(dependencies);
return this;
}
public SerializationTester addCodec(ObjectCodec<?> codec) {
additionalCodecs.add(codec);
return this;
}
public SerializationTester makeMemoizing() {
this.memoize = true;
return this;
}
public SerializationTester makeMemoizingAndAllowFutureBlocking(boolean allowFutureBlocking) {
makeMemoizing();
this.allowFutureBlocking = allowFutureBlocking;
return this;
}
public SerializationTester setObjectCodecs(ObjectCodecs objectCodecs) {
this.objectCodecs = objectCodecs;
return this;
}
public <T> SerializationTester setVerificationFunction(
VerificationFunction<T> verificationFunction) {
this.verificationFunction = verificationFunction;
return this;
}
/** Sets the number of times to repeat serialization and deserialization. */
public SerializationTester setRepetitions(int repetitions) {
this.repetitions = repetitions;
return this;
}
private void runTests(boolean verifyStableSerialization) throws Exception {
ObjectCodecs codecs = this.objectCodecs == null ? createObjectCodecs() : this.objectCodecs;
testSerializeDeserialize(codecs);
if (verifyStableSerialization) {
testStableSerialization(codecs);
}
testDeserializeJunkData(codecs);
}
public void runTests() throws Exception {
runTests(true);
}
/**
* Runs serialization tests without checking for stable serialization ({@code
* serialize(deserialize(serialize(x))) == serialize(x)}). Call {@link #runTests()}} instead if
* possible.
*
* <p>To be used only when serialization is not stable for good reasons: please understand the
* cause before using this. Typically unstable serialization is the result of non-determinism in
* your underlying objects, which can cause problems throughout Blaze by harming incrementality.
* Only if you are sure that the non-determinism in your objects is not detectable in its public
* interface or behavior (including {@code equals} if implemented) should you use this instead of
* {@link #runTests()}.
*/
public void runTestsWithoutStableSerializationCheck() throws Exception {
runTests(false);
}
private ObjectCodecs createObjectCodecs() {
ObjectCodecRegistry registry = AutoRegistry.get();
ImmutableClassToInstanceMap<Object> dependencies = dependenciesBuilder.build();
ObjectCodecRegistry.Builder registryBuilder = registry.getBuilder();
for (Object val : dependencies.values()) {
registryBuilder.addReferenceConstant(val);
}
for (ObjectCodec<?> codec : additionalCodecs) {
registryBuilder.add(codec);
}
return new ObjectCodecs(registryBuilder.build(), dependencies);
}
private ByteString serialize(Object subject, ObjectCodecs codecs) throws SerializationException {
if (memoize) {
if (allowFutureBlocking) {
return codecs.serializeMemoizedAndBlocking(subject).getObject();
} else {
return codecs.serializeMemoized(subject);
}
} else {
return codecs.serialize(subject);
}
}
private Object deserialize(ByteString serialized, ObjectCodecs codecs)
throws SerializationException {
if (memoize) {
return codecs.deserializeMemoized(serialized);
} else {
return codecs.deserialize(serialized);
}
}
/** Runs serialization/deserialization tests. */
@SuppressWarnings("unchecked")
private void testSerializeDeserialize(ObjectCodecs codecs) throws Exception {
Stopwatch timer = Stopwatch.createStarted();
int totalBytes = 0;
for (int i = 0; i < repetitions; ++i) {
for (Object subject : subjects) {
ByteString serialized = serialize(subject, codecs);
totalBytes += serialized.size();
Object deserialized = deserialize(serialized, codecs);
verificationFunction.verifyDeserialized(subject, deserialized);
}
}
logger.atInfo().log(
"%s total serialized bytes = %d, %s",
subjects.get(0).getClass().getSimpleName(), totalBytes, timer);
}
/** Runs serialized bytes stability tests. */
private void testStableSerialization(ObjectCodecs codecs) throws SerializationException {
for (Object subject : subjects) {
ByteString serialized = serialize(subject, codecs);
Object deserialized = deserialize(serialized, codecs);
ByteString reserialized = serialize(deserialized, codecs);
assertThat(reserialized).isEqualTo(serialized);
}
}
/** Runs junk-data recognition tests. */
private void testDeserializeJunkData(ObjectCodecs codecs) {
Random rng = new Random(0);
for (int i = 0; i < DEFAULT_JUNK_INPUTS; ++i) {
byte[] junkData = new byte[rng.nextInt(JUNK_LENGTH_UPPER_BOUND)];
rng.nextBytes(junkData);
try {
deserialize(ByteString.copyFrom(junkData), codecs);
// OK. Junk string was coincidentally parsed.
} catch (SerializationException e) {
// OK. Deserialization of junk failed.
return;
}
}
assertWithMessage("all junk was parsed successfully").fail();
}
}