From 06576ed13b038da71a23d0cd01b49fc1fd240625 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Thu, 22 Dec 2016 10:49:18 +0000 Subject: [PATCH] Adds abstract test classes for serialisation (#22281) This adds test classes that can be used to test the wire serialisation and (optionally) the XContent serialisation of objects that implement Streamable/Writeable and ToXContent. These test classes will enable classes sich as InternalAggregation (or at least its implementations) to be tested in a consistent way when is comes to testing serialisation. --- .../aggregations/InternalAggregation.java | 43 ++++++ .../InternalNumericMetricsAggregation.java | 13 ++ .../aggregations/metrics/min/InternalMin.java | 12 ++ .../metrics/min/InternalMinTests.java | 62 +++++++++ .../test/AbstractSerializingTestCase.java | 104 ++++++++++++++ .../test/AbstractStreamableTestCase.java | 127 ++++++++++++++++++ .../AbstractStreamableXContentTestCase.java | 102 ++++++++++++++ .../test/AbstractWireSerializingTestCase.java | 123 +++++++++++++++++ 8 files changed, 586 insertions(+) create mode 100644 core/src/test/java/org/elasticsearch/search/aggregations/metrics/min/InternalMinTests.java create mode 100644 test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java create mode 100644 test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableTestCase.java create mode 100644 test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableXContentTestCase.java create mode 100644 test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java b/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java index df25f0e2635..3b73d8b13d7 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/InternalAggregation.java @@ -32,6 +32,7 @@ import org.elasticsearch.search.aggregations.support.AggregationPath; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; /** * An internal implementation of {@link Aggregation}. Serves as a base class for all aggregation implementations. @@ -189,6 +190,48 @@ public abstract class InternalAggregation implements Aggregation, ToXContent, Na public abstract XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException; + @Override + public int hashCode() { + return Objects.hash(name, metaData, pipelineAggregators, doHashCode()); + } + + // norelease: make this abstract when all InternalAggregations implement this method + /** + * Opportunity for subclasses to the {@link #hashCode(Object)} for this + * class. + **/ + protected int doHashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj.getClass() != getClass()) { + return false; + } + InternalAggregation other = (InternalAggregation) obj; + return Objects.equals(name, other.name) && + Objects.equals(pipelineAggregators, other.pipelineAggregators) && + Objects.equals(metaData, other.metaData) && + doEquals(obj); + } + + // norelease: make this abstract when all InternalAggregations implement this method + /** + * Opportunity for subclasses to add criteria to the {@link #equals(Object)} + * method for this class. + * + * This method can safely cast obj to the subclass since the + * {@link #equals(Object)} method checks that obj is the same + * class as this + */ + protected boolean doEquals(Object obj) { + return this == obj; + } + /** * Common xcontent fields that are shared among addAggregation */ diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java index 9f591c7d425..b9442637a6b 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java @@ -25,6 +25,7 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; public abstract class InternalNumericMetricsAggregation extends InternalMetricsAggregation { @@ -102,4 +103,16 @@ public abstract class InternalNumericMetricsAggregation extends InternalMetricsA protected InternalNumericMetricsAggregation(StreamInput in) throws IOException { super(in); } + + @Override + protected int doHashCode() { + return Objects.hash(format, super.hashCode()); + } + + @Override + protected boolean doEquals(Object obj) { + InternalNumericMetricsAggregation other = (InternalNumericMetricsAggregation) obj; + return super.equals(obj) && + Objects.equals(format, other.format); + } } diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java index 09f9c3915fd..46481d1837f 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java @@ -29,6 +29,7 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; public class InternalMin extends InternalNumericMetricsAggregation.SingleValue implements Min { private final double min; @@ -89,4 +90,15 @@ public class InternalMin extends InternalNumericMetricsAggregation.SingleValue i return builder; } + @Override + protected int doHashCode() { + return Objects.hash(min); + } + + @Override + protected boolean doEquals(Object obj) { + InternalMin other = (InternalMin) obj; + return Objects.equals(min, other.min); + } + } diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/metrics/min/InternalMinTests.java b/core/src/test/java/org/elasticsearch/search/aggregations/metrics/min/InternalMinTests.java new file mode 100644 index 00000000000..dae49ff7fc3 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/search/aggregations/metrics/min/InternalMinTests.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.search.aggregations.metrics.min; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry.Entry; +import org.elasticsearch.common.io.stream.Writeable.Reader; +import org.elasticsearch.search.DocValueFormat; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class InternalMinTests extends AbstractWireSerializingTestCase { + + @Override + protected InternalMin createTestInstance() { + return new InternalMin(randomAsciiOfLengthBetween(1, 20), randomDouble(), + randomFrom(DocValueFormat.BOOLEAN, DocValueFormat.GEOHASH, DocValueFormat.IP, DocValueFormat.RAW), Collections.emptyList(), + new HashMap<>()); + } + + @Override + protected Reader instanceReader() { + return InternalMin::new; + } + + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + List entries = new ArrayList<>(); + entries.add(new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.BOOLEAN.getWriteableName(), + in -> DocValueFormat.BOOLEAN)); + entries.add(new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.DateTime.NAME, DocValueFormat.DateTime::new)); + entries.add(new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.Decimal.NAME, DocValueFormat.Decimal::new)); + entries.add(new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.GEOHASH.getWriteableName(), + in -> DocValueFormat.GEOHASH)); + entries.add(new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.IP.getWriteableName(), in -> DocValueFormat.IP)); + entries.add( + new NamedWriteableRegistry.Entry(DocValueFormat.class, DocValueFormat.RAW.getWriteableName(), in -> DocValueFormat.RAW)); + return new NamedWriteableRegistry(entries); + } + +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java new file mode 100644 index 00000000000..8d5556eeabb --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializingTestCase.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.test; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +public abstract class AbstractSerializingTestCase extends AbstractWireSerializingTestCase { + + /** + * Generic test that creates new instance from the test instance and checks + * both for equality and asserts equality on the two instances. + */ + public void testFromXContent() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T testInstance = createTestInstance(); + XContentType xContentType = randomFrom(XContentType.values()); + XContentBuilder builder = toXContent(testInstance, xContentType); + XContentBuilder shuffled = shuffleXContent(builder); + assertParsedInstance(xContentType, shuffled.bytes(), testInstance); + for (Map.Entry alternateVersion : getAlternateVersions().entrySet()) { + String instanceAsString = alternateVersion.getKey(); + assertParsedInstance(XContentType.JSON, new BytesArray(instanceAsString), alternateVersion.getValue()); + } + } + } + + private void assertParsedInstance(XContentType xContentType, BytesReference instanceAsBytes, T expectedInstance) + throws IOException { + + XContentParser parser = createParser(XContentFactory.xContent(xContentType), instanceAsBytes); + T newInstance = parseInstance(parser); + assertNotSame(newInstance, expectedInstance); + assertEquals(expectedInstance, newInstance); + assertEquals(expectedInstance.hashCode(), newInstance.hashCode()); + } + + private T parseInstance(XContentParser parser) throws IOException { + T parsedInstance = doParseInstance(parser); + assertNull(parser.nextToken()); + return parsedInstance; + } + + /** + * Parses to a new instance using the provided {@link XContentParser} + */ + protected abstract T doParseInstance(XContentParser parser); + + /** + * Renders the provided instance in XContent + * + * @param instance + * the instance to render + * @param contentType + * the content type to render to + */ + protected XContentBuilder toXContent(T instance, XContentType contentType) + throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(contentType); + if (randomBoolean()) { + builder.prettyPrint(); + } + instance.toXContent(builder, ToXContent.EMPTY_PARAMS); + return builder; + } + + /** + * Returns alternate string representation of the instance that need to be + * tested as they are never used as output of the test instance. By default + * there are no alternate versions. + * + * These alternatives must be JSON strings. + */ + protected Map getAlternateVersions() { + return Collections.emptyMap(); + } + +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableTestCase.java new file mode 100644 index 00000000000..0a6d7eca270 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableTestCase.java @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.test; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.Streamable; + +import java.io.IOException; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +public abstract class AbstractStreamableTestCase extends ESTestCase { + protected static final int NUMBER_OF_TEST_RUNS = 20; + + /** + * Creates a random test instance to use in the tests. This method will be + * called multiple times during test execution and should return a different + * random instance each time it is called. + */ + protected abstract T createTestInstance(); + + /** + * Creates an empty instance to use when deserialising the + * {@link Streamable}. This usually returns an instance created using the + * zer-arg constructor + */ + protected abstract T createBlankInstance(); + + /** + * Tests that the equals and hashcode methods are consistent and copied + * versions of the instance have are equal. + */ + public void testEqualsAndHashcode() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T firstInstance = createTestInstance(); + assertFalse("instance is equal to null", firstInstance.equals(null)); + assertFalse("instance is equal to incompatible type", firstInstance.equals("")); + assertEquals("instance is not equal to self", firstInstance, firstInstance); + assertThat("same instance's hashcode returns different values if called multiple times", firstInstance.hashCode(), + equalTo(firstInstance.hashCode())); + + T secondInstance = copyInstance(firstInstance); + assertEquals("instance is not equal to self", secondInstance, secondInstance); + assertEquals("instance is not equal to its copy", firstInstance, secondInstance); + assertEquals("equals is not symmetric", secondInstance, firstInstance); + assertThat("instance copy's hashcode is different from original hashcode", secondInstance.hashCode(), + equalTo(firstInstance.hashCode())); + + T thirdInstance = copyInstance(secondInstance); + assertEquals("instance is not equal to self", thirdInstance, thirdInstance); + assertEquals("instance is not equal to its copy", secondInstance, thirdInstance); + assertThat("instance copy's hashcode is different from original hashcode", secondInstance.hashCode(), + equalTo(thirdInstance.hashCode())); + assertEquals("equals is not transitive", firstInstance, thirdInstance); + assertThat("instance copy's hashcode is different from original hashcode", firstInstance.hashCode(), + equalTo(thirdInstance.hashCode())); + assertEquals("equals is not symmetric", thirdInstance, secondInstance); + assertEquals("equals is not symmetric", thirdInstance, firstInstance); + } + } + + /** + * Test serialization and deserialization of the test instance. + */ + public void testSerialization() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T testInstance = createTestInstance(); + assertSerialization(testInstance); + } + } + + /** + * Serialize the given instance and asserts that both are equal + */ + protected T assertSerialization(T testInstance) throws IOException { + T deserializedInstance = copyInstance(testInstance); + assertEquals(testInstance, deserializedInstance); + assertEquals(testInstance.hashCode(), deserializedInstance.hashCode()); + assertNotSame(testInstance, deserializedInstance); + return deserializedInstance; + } + + private T copyInstance(T instance) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + instance.writeTo(output); + try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), + getNamedWriteableRegistry())) { + T newInstance = createBlankInstance(); + newInstance.readFrom(in); + return newInstance; + } + } + } + + /** + * Get the {@link NamedWriteableRegistry} to use when de-serializing the object. + * + * Override this method if you need to register {@link NamedWriteable}s for the test object to de-serialize. + * + * By default this will return a {@link NamedWriteableRegistry} with no registered {@link NamedWriteable}s + */ + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Collections.emptyList()); + } + +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableXContentTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableXContentTestCase.java new file mode 100644 index 00000000000..79bd9d09223 --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractStreamableXContentTestCase.java @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.test; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Streamable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +public abstract class AbstractStreamableXContentTestCase extends AbstractStreamableTestCase { + + /** + * Generic test that creates new instance from the test instance and checks + * both for equality and asserts equality on the two queries. + */ + public void testFromXContent() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T testInstance = createTestInstance(); + XContentType xContentType = randomFrom(XContentType.values()); + XContentBuilder builder = toXContent(testInstance, xContentType); + XContentBuilder shuffled = shuffleXContent(builder); + assertParsedInstance(xContentType, shuffled.bytes(), testInstance); + for (Map.Entry alternateVersion : getAlternateVersions().entrySet()) { + String instanceAsString = alternateVersion.getKey(); + assertParsedInstance(XContentType.JSON, new BytesArray(instanceAsString), alternateVersion.getValue()); + } + } + } + + private void assertParsedInstance(XContentType xContentType, BytesReference instanceAsBytes, T expectedInstance) + throws IOException { + XContentParser parser = createParser(XContentFactory.xContent(xContentType), instanceAsBytes); + T newInstance = parseInstance(parser); + assertNotSame(newInstance, expectedInstance); + assertEquals(expectedInstance, newInstance); + assertEquals(expectedInstance.hashCode(), newInstance.hashCode()); + } + + private T parseInstance(XContentParser parser) throws IOException { + T parsedInstance = doParseInstance(parser); + assertNull(parser.nextToken()); + return parsedInstance; + } + + /** + * Parses to a new instance using the provided {@link XContentParser} + */ + protected abstract T doParseInstance(XContentParser parser); + + /** + * Renders the provided instance in XContent + * + * @param instance + * the instance to render + * @param contentType + * the content type to render to + */ + protected static XContentBuilder toXContent(T instance, XContentType contentType) + throws IOException { + XContentBuilder builder = XContentFactory.contentBuilder(contentType); + if (randomBoolean()) { + builder.prettyPrint(); + } + instance.toXContent(builder, ToXContent.EMPTY_PARAMS); + return builder; + } + + /** + * Returns alternate string representation of the instance that need to be + * tested as they are never used as output of the test instance. By default + * there are no alternate versions. + * + * These alternatives must be JSON strings. + */ + protected Map getAlternateVersions() { + return Collections.emptyMap(); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java new file mode 100644 index 00000000000..bf65a7f4bdd --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractWireSerializingTestCase.java @@ -0,0 +1,123 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.test; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.io.stream.Writeable.Reader; + +import java.io.IOException; +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; + +public abstract class AbstractWireSerializingTestCase extends ESTestCase { + protected static final int NUMBER_OF_TEST_RUNS = 20; + + /** + * Creates a random test instance to use in the tests. This method will be + * called multiple times during test execution and should return a different + * random instance each time it is called. + */ + protected abstract T createTestInstance(); + + /** + * Returns a {@link Reader} that can be used to de-serialize the instance + */ + protected abstract Reader instanceReader(); + + /** + * Tests that the equals and hashcode methods are consistent and copied + * versions of the instance have are equal. + */ + public void testEqualsAndHashcode() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T firstInstance = createTestInstance(); + assertFalse("instance is equal to null", firstInstance.equals(null)); + assertFalse("instance is equal to incompatible type", firstInstance.equals("")); + assertEquals("instance is not equal to self", firstInstance, firstInstance); + assertThat("same instance's hashcode returns different values if called multiple times", firstInstance.hashCode(), + equalTo(firstInstance.hashCode())); + + T secondInstance = copyInstance(firstInstance); + assertEquals("instance is not equal to self", secondInstance, secondInstance); + assertEquals("instance is not equal to its copy", firstInstance, secondInstance); + assertEquals("equals is not symmetric", secondInstance, firstInstance); + assertThat("instance copy's hashcode is different from original hashcode", secondInstance.hashCode(), + equalTo(firstInstance.hashCode())); + + T thirdInstance = copyInstance(secondInstance); + assertEquals("instance is not equal to self", thirdInstance, thirdInstance); + assertEquals("instance is not equal to its copy", secondInstance, thirdInstance); + assertThat("instance copy's hashcode is different from original hashcode", secondInstance.hashCode(), + equalTo(thirdInstance.hashCode())); + assertEquals("equals is not transitive", firstInstance, thirdInstance); + assertThat("instance copy's hashcode is different from original hashcode", firstInstance.hashCode(), + equalTo(thirdInstance.hashCode())); + assertEquals("equals is not symmetric", thirdInstance, secondInstance); + assertEquals("equals is not symmetric", thirdInstance, firstInstance); + } + } + + /** + * Test serialization and deserialization of the test instance. + */ + public void testSerialization() throws IOException { + for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) { + T testInstance = createTestInstance(); + assertSerialization(testInstance); + } + } + + /** + * Serialize the given instance and asserts that both are equal + */ + protected T assertSerialization(T testInstance) throws IOException { + T deserializedInstance = copyInstance(testInstance); + assertEquals(testInstance, deserializedInstance); + assertEquals(testInstance.hashCode(), deserializedInstance.hashCode()); + assertNotSame(testInstance, deserializedInstance); + return deserializedInstance; + } + + private T copyInstance(T instance) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + instance.writeTo(output); + try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), + getNamedWriteableRegistry())) { + return instanceReader().read(in); + } + } + } + + /** + * Get the {@link NamedWriteableRegistry} to use when de-serializing the object. + * + * Override this method if you need to register {@link NamedWriteable}s for the test object to de-serialize. + * + * By default this will return a {@link NamedWriteableRegistry} with no registered {@link NamedWriteable}s + */ + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Collections.emptyList()); + } +}