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.
This commit is contained in:
Colin Goodheart-Smithe 2016-12-22 10:49:18 +00:00 committed by GitHub
parent 7946396fe6
commit 06576ed13b
8 changed files with 586 additions and 0 deletions

View File

@ -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 <code>obj</code> to the subclass since the
* {@link #equals(Object)} method checks that <code>obj</code> is the same
* class as <code>this</code>
*/
protected boolean doEquals(Object obj) {
return this == obj;
}
/**
* Common xcontent fields that are shared among addAggregation
*/

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<InternalMin> {
@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<InternalMin> instanceReader() {
return InternalMin::new;
}
@Override
protected NamedWriteableRegistry getNamedWriteableRegistry() {
List<Entry> 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);
}
}

View File

@ -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<T extends ToXContent & Writeable> extends AbstractWireSerializingTestCase<T> {
/**
* 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<String, T> 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<String, T> getAlternateVersions() {
return Collections.emptyMap();
}
}

View File

@ -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<T extends Streamable> 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());
}
}

View File

@ -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<T extends ToXContent & Streamable> extends AbstractStreamableTestCase<T> {
/**
* 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<String, T> 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 <T extends ToXContent> 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<String, T> getAlternateVersions() {
return Collections.emptyMap();
}
}

View File

@ -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<T extends Writeable> 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<T> 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());
}
}