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());
+ }
+}