[TEST] share code between streamable/writeable/xcontent base test classes (#28785)

Today we have two test base classes that have a lot in common when it comes to testing wire and xcontent serialization: `AbstractSerializingTestCase` and `AbstractXContentStreamableTestCase`. There are subtle differences though between the two, in the way they work, what can be overridden and features that they support (e.g. insertion of random fields).

This commit introduces a new base class called `AbstractWireTestCase` which holds all of the serialization test code in common between `Streamable` and `Writeable`. It has two minimal subclasses called `AbstractWireSerializingTestCase` and `AbstractStreamableTestCase` which are specialized for `Writeable` and `Streamable`.

This commit also introduces a new test class called `AbstractXContentTestCase` for all of the xContent testing, which holds a testFromXContent method for parsing and rendering to xContent. This one can be delegated to from the existing `AbstractStreamableXContentTestCase` and `AbstractSerializingTestCase` so that we avoid code duplicate as much as possible and all these base classes offer the same functionalities in the same way. Having this last base class decoupled from the serialization testing may also help with the REST high-level client testing, as there are some classes where it's hard to implement equals/hashcode and this makes it possible to override `assertEqualInstances` for custom equality comparisons (also this base class doesn't require implementing equals/hashcode as it doesn't test such methods.
This commit is contained in:
Luca Cavanna 2018-02-23 10:48:48 +01:00 committed by GitHub
parent 3728c50d85
commit cd3d9c9f80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 524 additions and 489 deletions

View File

@ -0,0 +1,28 @@
/*
* 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.common;
/**
* A {@link java.util.function.BiFunction}-like interface which allows throwing checked exceptions.
*/
@FunctionalInterface
public interface CheckedBiFunction<T, U, R, E extends Exception> {
R apply(T t, U u) throws E;
}

View File

@ -205,8 +205,8 @@ public final class PersistentTasksCustomMetaData extends AbstractNamedDiffable<M
return ALL_CONTEXTS;
}
public static PersistentTasksCustomMetaData fromXContent(XContentParser parser) throws IOException {
return PERSISTENT_TASKS_PARSER.parse(parser, null).build();
public static PersistentTasksCustomMetaData fromXContent(XContentParser parser) {
return PERSISTENT_TASKS_PARSER.apply(parser, null).build();
}
@SuppressWarnings("unchecked")

View File

@ -318,7 +318,7 @@ public class StoredScriptSource extends AbstractDiffable<StoredScriptSource> imp
* Note that the "source" parameter can also handle template parsing including from
* a complex JSON object.
*/
public static StoredScriptSource fromXContent(XContentParser parser) throws IOException {
public static StoredScriptSource fromXContent(XContentParser parser) {
return PARSER.apply(parser, null).build();
}

View File

@ -121,9 +121,8 @@ public class CollapseBuilder implements Writeable, ToXContentObject {
}
}
public static CollapseBuilder fromXContent(XContentParser parser) throws IOException {
CollapseBuilder builder = PARSER.parse(parser, new CollapseBuilder(), null);
return builder;
public static CollapseBuilder fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}
// for object parser only

View File

@ -25,7 +25,6 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.Settings.Builder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import java.util.List;
import java.util.Set;
@ -39,23 +38,21 @@ public class ClusterUpdateSettingsResponseTests extends AbstractStreamableXConte
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<ClusterUpdateSettingsResponse> getMutateFunction() {
return response -> {
int i = randomIntBetween(0, 2);
switch(i) {
case 0:
return new ClusterUpdateSettingsResponse(response.isAcknowledged() == false,
response.transientSettings, response.persistentSettings);
case 1:
return new ClusterUpdateSettingsResponse(response.isAcknowledged(), mutateSettings(response.transientSettings),
response.persistentSettings);
case 2:
return new ClusterUpdateSettingsResponse(response.isAcknowledged(), response.transientSettings,
mutateSettings(response.persistentSettings));
default:
throw new UnsupportedOperationException();
}
};
protected ClusterUpdateSettingsResponse mutateInstance(ClusterUpdateSettingsResponse response) {
int i = randomIntBetween(0, 2);
switch(i) {
case 0:
return new ClusterUpdateSettingsResponse(response.isAcknowledged() == false,
response.transientSettings, response.persistentSettings);
case 1:
return new ClusterUpdateSettingsResponse(response.isAcknowledged(), mutateSettings(response.transientSettings),
response.persistentSettings);
case 2:
return new ClusterUpdateSettingsResponse(response.isAcknowledged(), response.transientSettings,
mutateSettings(response.persistentSettings));
default:
throw new UnsupportedOperationException();
}
}
private static Settings mutateSettings(Settings settings) {

View File

@ -21,7 +21,6 @@ package org.elasticsearch.action.admin.indices.alias;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class IndicesAliasesResponseTests extends AbstractStreamableXContentTestCase<IndicesAliasesResponse> {
@ -41,7 +40,7 @@ public class IndicesAliasesResponseTests extends AbstractStreamableXContentTestC
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<IndicesAliasesResponse> getMutateFunction() {
return response -> new IndicesAliasesResponse(response.isAcknowledged() == false);
protected IndicesAliasesResponse mutateInstance(IndicesAliasesResponse response) {
return new IndicesAliasesResponse(response.isAcknowledged() == false);
}
}

View File

@ -21,7 +21,6 @@ package org.elasticsearch.action.admin.indices.close;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class CloseIndexResponseTests extends AbstractStreamableXContentTestCase<CloseIndexResponse> {
@ -41,7 +40,7 @@ public class CloseIndexResponseTests extends AbstractStreamableXContentTestCase<
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<CloseIndexResponse> getMutateFunction() {
return response -> new CloseIndexResponse(response.isAcknowledged() == false);
protected CloseIndexResponse mutateInstance(CloseIndexResponse response) {
return new CloseIndexResponse(response.isAcknowledged() == false);
}
}

View File

@ -26,7 +26,6 @@ import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import java.io.IOException;
@ -46,23 +45,21 @@ public class CreateIndexResponseTests extends AbstractStreamableXContentTestCase
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<CreateIndexResponse> getMutateFunction() {
return response -> {
protected CreateIndexResponse mutateInstance(CreateIndexResponse response) {
if (randomBoolean()) {
if (randomBoolean()) {
if (randomBoolean()) {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new CreateIndexResponse(acknowledged, shardsAcknowledged, response.index());
} else {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new CreateIndexResponse(acknowledged, shardsAcknowledged, response.index());
}
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new CreateIndexResponse(acknowledged, shardsAcknowledged, response.index());
} else {
return new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(),
response.index() + randomAlphaOfLengthBetween(2, 5));
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new CreateIndexResponse(acknowledged, shardsAcknowledged, response.index());
}
};
} else {
return new CreateIndexResponse(response.isAcknowledged(), response.isShardsAcknowledged(),
response.index() + randomAlphaOfLengthBetween(2, 5));
}
}
@Override

View File

@ -22,7 +22,6 @@ package org.elasticsearch.action.admin.indices.delete;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class DeleteIndexResponseTests extends AbstractStreamableXContentTestCase<DeleteIndexResponse> {
@ -48,7 +47,7 @@ public class DeleteIndexResponseTests extends AbstractStreamableXContentTestCase
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<DeleteIndexResponse> getMutateFunction() {
return response -> new DeleteIndexResponse(response.isAcknowledged() == false);
protected DeleteIndexResponse mutateInstance(DeleteIndexResponse response) {
return new DeleteIndexResponse(response.isAcknowledged() == false);
}
}

View File

@ -22,7 +22,6 @@ package org.elasticsearch.action.admin.indices.mapping.put;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class PutMappingResponseTests extends AbstractStreamableXContentTestCase<PutMappingResponse> {
@ -48,7 +47,7 @@ public class PutMappingResponseTests extends AbstractStreamableXContentTestCase<
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<PutMappingResponse> getMutateFunction() {
return response -> new PutMappingResponse(response.isAcknowledged() == false);
protected PutMappingResponse mutateInstance(PutMappingResponse response) {
return new PutMappingResponse(response.isAcknowledged() == false);
}
}

View File

@ -21,7 +21,6 @@ package org.elasticsearch.action.admin.indices.open;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class OpenIndexResponseTests extends AbstractStreamableXContentTestCase<OpenIndexResponse> {
@ -43,17 +42,15 @@ public class OpenIndexResponseTests extends AbstractStreamableXContentTestCase<O
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<OpenIndexResponse> getMutateFunction() {
return response -> {
if (randomBoolean()) {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new OpenIndexResponse(acknowledged, shardsAcknowledged);
} else {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new OpenIndexResponse(acknowledged, shardsAcknowledged);
}
};
protected OpenIndexResponse mutateInstance(OpenIndexResponse response) {
if (randomBoolean()) {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new OpenIndexResponse(acknowledged, shardsAcknowledged);
} else {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new OpenIndexResponse(acknowledged, shardsAcknowledged);
}
}
}

View File

@ -22,7 +22,6 @@ package org.elasticsearch.action.admin.indices.rollover;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import java.util.ArrayList;
import java.util.HashMap;
@ -75,58 +74,56 @@ public class RolloverResponseTests extends AbstractStreamableXContentTestCase<Ro
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<RolloverResponse> getMutateFunction() {
return response -> {
int i = randomIntBetween(0, 6);
switch(i) {
case 0:
return new RolloverResponse(response.getOldIndex() + randomAlphaOfLengthBetween(2, 5),
response.getNewIndex(), response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 1:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex() + randomAlphaOfLengthBetween(2, 5),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 2:
Map<String, Boolean> results;
if (response.getConditionStatus().isEmpty()) {
results = randomResults(false);
} else {
results = new HashMap<>(response.getConditionStatus().size());
List<String> keys = randomSubsetOf(randomIntBetween(1, response.getConditionStatus().size()),
response.getConditionStatus().keySet());
for (Map.Entry<String, Boolean> entry : response.getConditionStatus().entrySet()) {
boolean value = keys.contains(entry.getKey()) ? entry.getValue() == false : entry.getValue();
results.put(entry.getKey(), value);
}
protected RolloverResponse mutateInstance(RolloverResponse response) {
int i = randomIntBetween(0, 6);
switch(i) {
case 0:
return new RolloverResponse(response.getOldIndex() + randomAlphaOfLengthBetween(2, 5),
response.getNewIndex(), response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 1:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex() + randomAlphaOfLengthBetween(2, 5),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 2:
Map<String, Boolean> results;
if (response.getConditionStatus().isEmpty()) {
results = randomResults(false);
} else {
results = new HashMap<>(response.getConditionStatus().size());
List<String> keys = randomSubsetOf(randomIntBetween(1, response.getConditionStatus().size()),
response.getConditionStatus().keySet());
for (Map.Entry<String, Boolean> entry : response.getConditionStatus().entrySet()) {
boolean value = keys.contains(entry.getKey()) ? entry.getValue() == false : entry.getValue();
results.put(entry.getKey(), value);
}
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(), results, response.isDryRun(),
response.isRolledOver(), response.isAcknowledged(), response.isShardsAcknowledged());
case 3:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun() == false, response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 4:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver() == false,
response.isAcknowledged(), response.isShardsAcknowledged());
case 5: {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
acknowledged, shardsAcknowledged);
}
case 6: {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
acknowledged, shardsAcknowledged);
}
default:
throw new UnsupportedOperationException();
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(), results, response.isDryRun(),
response.isRolledOver(), response.isAcknowledged(), response.isShardsAcknowledged());
case 3:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun() == false, response.isRolledOver(),
response.isAcknowledged(), response.isShardsAcknowledged());
case 4:
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver() == false,
response.isAcknowledged(), response.isShardsAcknowledged());
case 5: {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
acknowledged, shardsAcknowledged);
}
};
case 6: {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new RolloverResponse(response.getOldIndex(), response.getNewIndex(),
response.getConditionStatus(), response.isDryRun(), response.isRolledOver(),
acknowledged, shardsAcknowledged);
}
default:
throw new UnsupportedOperationException();
}
}
}

View File

@ -22,7 +22,6 @@ package org.elasticsearch.action.admin.indices.shrink;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
public class ResizeResponseTests extends AbstractStreamableXContentTestCase<ResizeResponse> {
@ -51,22 +50,20 @@ public class ResizeResponseTests extends AbstractStreamableXContentTestCase<Resi
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<ResizeResponse> getMutateFunction() {
return response -> {
protected ResizeResponse mutateInstance(ResizeResponse response) {
if (randomBoolean()) {
if (randomBoolean()) {
if (randomBoolean()) {
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new ResizeResponse(acknowledged, shardsAcknowledged, response.index());
} else {
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new ResizeResponse(acknowledged, shardsAcknowledged, response.index());
}
boolean acknowledged = response.isAcknowledged() == false;
boolean shardsAcknowledged = acknowledged && response.isShardsAcknowledged();
return new ResizeResponse(acknowledged, shardsAcknowledged, response.index());
} else {
return new ResizeResponse(response.isAcknowledged(), response.isShardsAcknowledged(),
response.index() + randomAlphaOfLengthBetween(2, 5));
boolean shardsAcknowledged = response.isShardsAcknowledged() == false;
boolean acknowledged = shardsAcknowledged || response.isAcknowledged();
return new ResizeResponse(acknowledged, shardsAcknowledged, response.index());
}
};
} else {
return new ResizeResponse(response.isAcknowledged(), response.isShardsAcknowledged(),
response.index() + randomAlphaOfLengthBetween(2, 5));
}
}
}

View File

@ -27,7 +27,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils;
import org.elasticsearch.test.VersionUtils;
import java.io.IOException;
@ -35,13 +34,6 @@ import java.util.Date;
public class MainResponseTests extends AbstractStreamableXContentTestCase<MainResponse> {
@Override
protected MainResponse getExpectedFromXContent(MainResponse testInstance) {
// we cannot recreate the "available" flag from xContent, but should be "true" if request came through
testInstance.available = true;
return testInstance;
}
@Override
protected MainResponse createTestInstance() {
String clusterUuid = randomAlphaOfLength(10);
@ -49,8 +41,7 @@ public class MainResponseTests extends AbstractStreamableXContentTestCase<MainRe
String nodeName = randomAlphaOfLength(10);
Build build = new Build(randomAlphaOfLength(8), new Date(randomNonNegativeLong()).toString(), randomBoolean());
Version version = VersionUtils.randomVersion(random());
boolean available = randomBoolean();
return new MainResponse(nodeName, version, clusterName, clusterUuid , build, available);
return new MainResponse(nodeName, version, clusterName, clusterUuid , build, true);
}
@Override
@ -87,36 +78,34 @@ public class MainResponseTests extends AbstractStreamableXContentTestCase<MainRe
}
@Override
protected EqualsHashCodeTestUtils.MutateFunction<MainResponse> getMutateFunction() {
return o -> {
String clusterUuid = o.getClusterUuid();
boolean available = o.isAvailable();
Build build = o.getBuild();
Version version = o.getVersion();
String nodeName = o.getNodeName();
ClusterName clusterName = o.getClusterName();
switch (randomIntBetween(0, 5)) {
case 0:
clusterUuid = clusterUuid + randomAlphaOfLength(5);
break;
case 1:
nodeName = nodeName + randomAlphaOfLength(5);
break;
case 2:
available = !available;
break;
case 3:
// toggle the snapshot flag of the original Build parameter
build = new Build(build.shortHash(), build.date(), !build.isSnapshot());
break;
case 4:
version = randomValueOtherThan(version, () -> VersionUtils.randomVersion(random()));
break;
case 5:
clusterName = new ClusterName(clusterName + randomAlphaOfLength(5));
break;
}
return new MainResponse(nodeName, version, clusterName, clusterUuid, build, available);
};
protected MainResponse mutateInstance(MainResponse mutateInstance) {
String clusterUuid = mutateInstance.getClusterUuid();
boolean available = mutateInstance.isAvailable();
Build build = mutateInstance.getBuild();
Version version = mutateInstance.getVersion();
String nodeName = mutateInstance.getNodeName();
ClusterName clusterName = mutateInstance.getClusterName();
switch (randomIntBetween(0, 5)) {
case 0:
clusterUuid = clusterUuid + randomAlphaOfLength(5);
break;
case 1:
nodeName = nodeName + randomAlphaOfLength(5);
break;
case 2:
available = !available;
break;
case 3:
// toggle the snapshot flag of the original Build parameter
build = new Build(build.shortHash(), build.date(), !build.isSnapshot());
break;
case 4:
version = randomValueOtherThan(version, () -> VersionUtils.randomVersion(random()));
break;
case 5:
clusterName = new ClusterName(clusterName + randomAlphaOfLength(5));
break;
}
return new MainResponse(nodeName, version, clusterName, clusterUuid, build, available);
}
}

View File

@ -20,7 +20,6 @@
package org.elasticsearch.index.refresh;
import org.elasticsearch.test.AbstractStreamableTestCase;
import org.elasticsearch.test.EqualsHashCodeTestUtils.MutateFunction;
public class RefreshStatsTests extends AbstractStreamableTestCase<RefreshStats> {
@Override
@ -34,24 +33,22 @@ public class RefreshStatsTests extends AbstractStreamableTestCase<RefreshStats>
}
@Override
protected MutateFunction<RefreshStats> getMutateFunction() {
return instance -> {
long total = instance.getTotal();
long totalInMillis = instance.getTotalTimeInMillis();
int listeners = instance.getListeners();
switch (randomInt(2)) {
case 0:
total += between(1, 2000);
break;
case 1:
totalInMillis += between(1, 2000);
break;
case 2:
default:
listeners += between(1, 2000);
break;
}
return new RefreshStats(total, totalInMillis, listeners);
};
protected RefreshStats mutateInstance(RefreshStats instance) {
long total = instance.getTotal();
long totalInMillis = instance.getTotalTimeInMillis();
int listeners = instance.getListeners();
switch (randomInt(2)) {
case 0:
total += between(1, 2000);
break;
case 1:
totalInMillis += between(1, 2000);
break;
case 2:
default:
listeners += between(1, 2000);
break;
}
return new RefreshStats(total, totalInMillis, listeners);
}
}

View File

@ -34,16 +34,15 @@ import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.test.AbstractDiffableSerializationTestCase;
import org.elasticsearch.persistent.PersistentTasksCustomMetaData.Assignment;
import org.elasticsearch.persistent.PersistentTasksCustomMetaData.Builder;
import org.elasticsearch.persistent.PersistentTasksCustomMetaData.PersistentTask;
import org.elasticsearch.persistent.TestPersistentTasksPlugin.Status;
import org.elasticsearch.persistent.TestPersistentTasksPlugin.TestParams;
import org.elasticsearch.persistent.TestPersistentTasksPlugin.TestPersistentTasksExecutor;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.test.AbstractDiffableSerializationTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -123,18 +122,10 @@ public class PersistentTasksCustomMetaDataTests extends AbstractDiffableSerializ
}
@Override
protected PersistentTasksCustomMetaData doParseInstance(XContentParser parser) throws IOException {
protected PersistentTasksCustomMetaData doParseInstance(XContentParser parser) {
return PersistentTasksCustomMetaData.fromXContent(parser);
}
/*
@Override
protected XContentBuilder toXContent(Custom instance, XContentType contentType) throws IOException {
return toXContent(instance, contentType, new ToXContent.MapParams(
Collections.singletonMap(MetaData.CONTEXT_MODE_PARAM, MetaData.XContentContext.API.toString())));
}
*/
private String addRandomTask(Builder builder) {
String taskId = UUIDs.base64UUID();
builder.addTask(taskId, TestPersistentTasksExecutor.NAME, new TestParams(randomAlphaOfLength(10)), randomAssignment());

View File

@ -55,7 +55,7 @@ public class StoredScriptSourceTests extends AbstractSerializingTestCase<StoredS
}
@Override
protected StoredScriptSource doParseInstance(XContentParser parser) throws IOException {
protected StoredScriptSource doParseInstance(XContentParser parser) {
return StoredScriptSource.fromXContent(parser);
}

View File

@ -27,8 +27,6 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.AbstractSerializingTestCase;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -218,15 +216,11 @@ public class StoredScriptTests extends AbstractSerializingTestCase<StoredScriptS
@Override
protected StoredScriptSource doParseInstance(XContentParser parser) {
try {
return StoredScriptSource.fromXContent(parser);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
return StoredScriptSource.fromXContent(parser);
}
@Override
protected StoredScriptSource mutateInstance(StoredScriptSource instance) throws IOException {
protected StoredScriptSource mutateInstance(StoredScriptSource instance) {
String source = instance.getSource();
String lang = instance.getLang();
Map<String, String> options = instance.getOptions();

View File

@ -19,14 +19,11 @@
package org.elasticsearch.search.aggregations;
import org.elasticsearch.Version;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.Writeable.Reader;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.Token;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.aggregations.InternalOrder.CompoundOrder;
import org.elasticsearch.test.AbstractSerializingTestCase;
import org.elasticsearch.test.VersionUtils;
@ -80,25 +77,9 @@ public class InternalOrderTests extends AbstractSerializingTestCase<BucketOrder>
}
@Override
protected BucketOrder assertSerialization(BucketOrder testInstance) throws IOException {
// identical behavior to AbstractWireSerializingTestCase, except assertNotSame is only called for
// compound and aggregation order because _key and _count orders are static instances.
BucketOrder deserializedInstance = copyInstance(testInstance);
assertEquals(testInstance, deserializedInstance);
assertEquals(testInstance.hashCode(), deserializedInstance.hashCode());
if(testInstance instanceof CompoundOrder || testInstance instanceof InternalOrder.Aggregation) {
assertNotSame(testInstance, deserializedInstance);
}
return deserializedInstance;
}
@Override
protected void assertParsedInstance(XContentType xContentType, BytesReference instanceAsBytes, BucketOrder expectedInstance)
throws IOException {
protected void assertEqualInstances(BucketOrder expectedInstance, BucketOrder newInstance) {
// identical behavior to AbstractSerializingTestCase, except assertNotSame is only called for
// compound and aggregation order because _key and _count orders are static instances.
XContentParser parser = createParser(XContentFactory.xContent(xContentType), instanceAsBytes);
BucketOrder newInstance = parseInstance(parser);
assertEquals(expectedInstance, newInstance);
assertEquals(expectedInstance.hashCode(), newInstance.hashCode());
if(expectedInstance instanceof CompoundOrder || expectedInstance instanceof InternalOrder.Aggregation) {

View File

@ -64,7 +64,7 @@ public class CollapseBuilderTests extends AbstractSerializingTestCase<CollapseBu
}
@AfterClass
public static void afterClass() throws Exception {
public static void afterClass() {
namedWriteableRegistry = null;
xContentRegistry = null;
}
@ -186,7 +186,7 @@ public class CollapseBuilderTests extends AbstractSerializingTestCase<CollapseBu
}
}
public void testBuildWithSearchContextExceptions() throws IOException {
public void testBuildWithSearchContextExceptions() {
SearchContext context = mockSearchContext();
{
CollapseBuilder builder = new CollapseBuilder("unknown_field");
@ -225,7 +225,7 @@ public class CollapseBuilderTests extends AbstractSerializingTestCase<CollapseBu
}
@Override
protected CollapseBuilder doParseInstance(XContentParser parser) throws IOException {
protected CollapseBuilder doParseInstance(XContentParser parser) {
return CollapseBuilder.fromXContent(parser);
}

View File

@ -19,14 +19,12 @@
package org.elasticsearch.test;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContent;
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.function.Predicate;
public abstract class AbstractSerializingTestCase<T extends ToXContent & Writeable> extends AbstractWireSerializingTestCase<T> {
@ -35,29 +33,9 @@ public abstract class AbstractSerializingTestCase<T extends ToXContent & Writeab
* both for equality and asserts equality on the two instances.
*/
public final void testFromXContent() throws IOException {
for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) {
T testInstance = createTestInstance();
XContentType xContentType = randomFrom(XContentType.values());
BytesReference shuffled = toShuffledXContent(testInstance, xContentType, ToXContent.EMPTY_PARAMS,
false, getShuffleFieldsExceptions());
assertParsedInstance(xContentType, shuffled, testInstance);
}
}
protected 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());
}
protected T parseInstance(XContentParser parser) throws IOException {
T parsedInstance = doParseInstance(parser);
assertNull(parser.nextToken());
return parsedInstance;
AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, this::createTestInstance, supportsUnknownFields(),
getShuffleFieldsExceptions(), getRandomFieldsExcludeFilter(), this::createParser, this::doParseInstance,
this::assertEqualInstances);
}
/**
@ -65,6 +43,21 @@ public abstract class AbstractSerializingTestCase<T extends ToXContent & Writeab
*/
protected abstract T doParseInstance(XContentParser parser) throws IOException;
/**
* Indicates whether the parser supports unknown fields or not. In case it does, such behaviour will be tested by
* inserting random fields before parsing and checking that they don't make parsing fail.
*/
protected boolean supportsUnknownFields() {
return false;
}
/**
* Returns a predicate that given the field name indicates whether the field has to be excluded from random fields insertion or not
*/
protected Predicate<String> getRandomFieldsExcludeFilter() {
return field -> false;
}
/**
* Fields that have to be ignored when shuffling as part of testFromXContent
*/

View File

@ -19,27 +19,22 @@
package org.elasticsearch.test;
import org.elasticsearch.Version;
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 org.elasticsearch.test.EqualsHashCodeTestUtils.CopyFunction;
import org.elasticsearch.test.EqualsHashCodeTestUtils.MutateFunction;
import org.elasticsearch.common.io.stream.Writeable;
import java.io.IOException;
import java.util.Collections;
public abstract class AbstractStreamableTestCase<T extends Streamable> extends ESTestCase {
protected static final int NUMBER_OF_TEST_RUNS = 20;
public abstract class AbstractStreamableTestCase<T extends Streamable> extends AbstractWireTestCase<T> {
/**
* 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();
@Override
protected final T copyInstance(T instance, Version version) throws IOException {
return copyStreamable(instance, getNamedWriteableRegistry(), this::createBlankInstance, version);
}
@Override
protected final Writeable.Reader<T> instanceReader() {
return Streamable.newWriteableReader(this::createBlankInstance);
}
/**
* Creates an empty instance to use when deserialising the
@ -47,83 +42,4 @@ public abstract class AbstractStreamableTestCase<T extends Streamable> extends E
* zer-arg constructor
*/
protected abstract T createBlankInstance();
/**
* Returns a {@link CopyFunction} that can be used to make an exact copy of
* the given instance. This defaults to a function that uses
* {@link #copyInstance(Streamable, Version)} to create the copy.
*/
protected CopyFunction<T> getCopyFunction() {
return (original) -> copyInstance(original, Version.CURRENT);
}
/**
* Returns a {@link MutateFunction} that can be used to create a copy
* of the given instance that is different to this instance. This defaults
* to null.
*/
// TODO: Make this abstract when all sub-classes implement this (https://github.com/elastic/elasticsearch/issues/25929)
protected MutateFunction<T> getMutateFunction() {
return null;
}
/**
* Tests that the equals and hashcode methods are consistent and copied
* versions of the instance have are equal.
*/
public final void testEqualsAndHashcode() {
for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) {
EqualsHashCodeTestUtils.checkEqualsAndHashCode(createTestInstance(), getCopyFunction(), getMutateFunction());
}
}
/**
* Test serialization and deserialization of the test instance.
*/
public final 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, Version.CURRENT);
assertEquals(testInstance, deserializedInstance);
assertEquals(testInstance.hashCode(), deserializedInstance.hashCode());
assertNotSame(testInstance, deserializedInstance);
return deserializedInstance;
}
/**
* Round trip {@code instance} through binary serialization, setting the wire compatibility version to {@code version}.
*/
private T copyInstance(T instance, Version version) throws IOException {
try (BytesStreamOutput output = new BytesStreamOutput()) {
output.setVersion(version);
instance.writeTo(output);
try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(),
getNamedWriteableRegistry())) {
in.setVersion(version);
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

@ -18,19 +18,14 @@
*/
package org.elasticsearch.test;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.Streamable;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import java.io.IOException;
import java.util.function.Predicate;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
public abstract class AbstractStreamableXContentTestCase<T extends ToXContent & Streamable> extends AbstractStreamableTestCase<T> {
/**
@ -38,34 +33,15 @@ public abstract class AbstractStreamableXContentTestCase<T extends ToXContent &
* both for equality and asserts equality on the two queries.
*/
public final void testFromXContent() throws IOException {
for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) {
T testInstance = createTestInstance();
XContentType xContentType = randomFrom(XContentType.values());
BytesReference shuffled = toShuffledXContent(testInstance, xContentType, ToXContent.EMPTY_PARAMS, false);
BytesReference withRandomFields;
if (supportsUnknownFields()) {
// we add a few random fields to check that parser is lenient on new fields
withRandomFields = XContentTestUtils.insertRandomFields(xContentType, shuffled, getRandomFieldsExcludeFilter(), random());
} else {
withRandomFields = shuffled;
}
XContentParser parser = createParser(XContentFactory.xContent(xContentType), withRandomFields);
T parsed = parseInstance(parser);
T expected = getExpectedFromXContent(testInstance);
assertNotSame(expected, parsed);
assertEquals(expected, parsed);
assertEquals(expected.hashCode(), parsed.hashCode());
assertToXContentEquivalent(shuffled, XContentHelper.toXContent(parsed, xContentType, false), xContentType);
}
AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, this::createTestInstance, supportsUnknownFields(),
getShuffleFieldsExceptions(), getRandomFieldsExcludeFilter(), this::createParser, this::doParseInstance,
this::assertEqualInstances);
}
/**
* Returns the expected parsed object given the test object that the parser will be fed with.
* Useful in cases some fields are not written as part of toXContent, hence not parsed back.
* Parses to a new instance using the provided {@link XContentParser}
*/
protected T getExpectedFromXContent(T testInstance) {
return testInstance;
}
protected abstract T doParseInstance(XContentParser parser) throws IOException;;
/**
* Indicates whether the parser supports unknown fields or not. In case it does, such behaviour will be tested by
@ -75,18 +51,17 @@ public abstract class AbstractStreamableXContentTestCase<T extends ToXContent &
return true;
}
/**
* Returns a predicate that given the field name indicates whether the field has to be excluded from random fields insertion or not
*/
protected Predicate<String> getRandomFieldsExcludeFilter() {
return field -> false;
}
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}
* Fields that have to be ignored when shuffling as part of testFromXContent
*/
protected abstract T doParseInstance(XContentParser parser);
protected String[] getShuffleFieldsExceptions() {
return Strings.EMPTY_ARRAY;
}
}

View File

@ -19,100 +19,14 @@
package org.elasticsearch.test;
import org.elasticsearch.Version;
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;
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();
/**
* Returns an instance which is mutated slightly so it should not be equal
* to the given instance.
*/
// TODO: Make this abstract when all sub-classes implement this (https://github.com/elastic/elasticsearch/issues/25929)
protected T mutateInstance(T instance) throws IOException {
return null;
}
/**
* Tests that the equals and hashcode methods are consistent and copied
* versions of the instance have are equal.
*/
public final void testEqualsAndHashcode() {
for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) {
EqualsHashCodeTestUtils.checkEqualsAndHashCode(createTestInstance(), this::copyInstance, this::mutateInstance);
}
}
/**
* Test serialization and deserialization of the test instance.
*/
public final 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 {
return assertSerialization(testInstance, Version.CURRENT);
}
protected T assertSerialization(T testInstance, Version version) throws IOException {
T deserializedInstance = copyInstance(testInstance, version);
assertEquals(testInstance, deserializedInstance);
assertEquals(testInstance.hashCode(), deserializedInstance.hashCode());
assertNotSame(testInstance, deserializedInstance);
return deserializedInstance;
}
protected T copyInstance(T instance) throws IOException {
return copyInstance(instance, Version.CURRENT);
}
public abstract class AbstractWireSerializingTestCase<T extends Writeable> extends AbstractWireTestCase<T> {
@Override
protected T copyInstance(T instance, Version version) throws IOException {
try (BytesStreamOutput output = new BytesStreamOutput()) {
output.setVersion(version);
instance.writeTo(output);
try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(),
getNamedWriteableRegistry())) {
in.setVersion(version);
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());
return copyWriteable(instance, getNamedWriteableRegistry(), instanceReader());
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.Version;
import org.elasticsearch.common.io.stream.NamedWriteable;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.io.stream.Writeable;
import java.io.IOException;
import java.util.Collections;
public abstract class AbstractWireTestCase<T> 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 Writeable.Reader} that can be used to de-serialize the instance
*/
protected abstract Writeable.Reader<T> instanceReader();
/**
* Returns an instance which is mutated slightly so it should not be equal
* to the given instance.
*/
// TODO: Make this abstract when all sub-classes implement this (https://github.com/elastic/elasticsearch/issues/25929)
protected T mutateInstance(T instance) throws IOException {
return null;
}
/**
* Tests that the equals and hashcode methods are consistent and copied
* versions of the instance have are equal.
*/
public final void testEqualsAndHashcode() {
for (int runs = 0; runs < NUMBER_OF_TEST_RUNS; runs++) {
EqualsHashCodeTestUtils.checkEqualsAndHashCode(createTestInstance(), this::copyInstance, this::mutateInstance);
}
}
/**
* Test serialization and deserialization of the test instance.
*/
public final 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 final T assertSerialization(T testInstance) throws IOException {
return assertSerialization(testInstance, Version.CURRENT);
}
protected final T assertSerialization(T testInstance, Version version) throws IOException {
T deserializedInstance = copyInstance(testInstance, version);
assertEqualInstances(testInstance, deserializedInstance);
return deserializedInstance;
}
protected void assertEqualInstances(T expectedInstance, T newInstance) {
assertNotSame(newInstance, expectedInstance);
assertEquals(expectedInstance, newInstance);
assertEquals(expectedInstance.hashCode(), newInstance.hashCode());
}
protected final T copyInstance(T instance) throws IOException {
return copyInstance(instance, Version.CURRENT);
}
protected abstract T copyInstance(T instance, Version version) throws IOException;
/**
* 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,122 @@
/*
* 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.CheckedBiFunction;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import java.io.IOException;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent;
public abstract class AbstractXContentTestCase<T extends ToXContent> extends ESTestCase {
public static <T extends ToXContent> void testFromXContent(int numberOfTestRuns, Supplier<T> instanceSupplier,
boolean supportsUnknownFields, String[] shuffleFieldsExceptions,
Predicate<String> randomFieldsExcludeFilter,
CheckedBiFunction<XContent, BytesReference, XContentParser, IOException>
createParserFunction,
CheckedFunction<XContentParser, T, IOException> parseFunction,
BiConsumer<T, T> assertEqualsConsumer) throws IOException {
for (int runs = 0; runs < numberOfTestRuns; runs++) {
T testInstance = instanceSupplier.get();
XContentType xContentType = randomFrom(XContentType.values());
BytesReference shuffled = toShuffledXContent(testInstance, xContentType, ToXContent.EMPTY_PARAMS, false, createParserFunction,
shuffleFieldsExceptions);
BytesReference withRandomFields;
if (supportsUnknownFields) {
// we add a few random fields to check that parser is lenient on new fields
withRandomFields = XContentTestUtils.insertRandomFields(xContentType, shuffled, randomFieldsExcludeFilter, random());
} else {
withRandomFields = shuffled;
}
XContentParser parser = createParserFunction.apply(XContentFactory.xContent(xContentType), withRandomFields);
T parsed = parseFunction.apply(parser);
assertEqualsConsumer.accept(testInstance, parsed);
assertToXContentEquivalent(shuffled, XContentHelper.toXContent(parsed, xContentType, false), xContentType);
}
}
/**
* Generic test that creates new instance from the test instance and checks
* both for equality and asserts equality on the two queries.
*/
public final void testFromXContent() throws IOException {
testFromXContent(numberOfTestRuns(), this::createTestInstance, supportsUnknownFields(), getShuffleFieldsExceptions(),
getRandomFieldsExcludeFilter(), this::createParser, this::parseInstance, this::assertEqualInstances);
}
protected abstract int numberOfTestRuns();
/**
* 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();
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) throws IOException;
protected void assertEqualInstances(T expectedInstance, T newInstance) {
assertNotSame(newInstance, expectedInstance);
assertEquals(expectedInstance, newInstance);
assertEquals(expectedInstance.hashCode(), newInstance.hashCode());
}
/**
* Indicates whether the parser supports unknown fields or not. In case it does, such behaviour will be tested by
* inserting random fields before parsing and checking that they don't make parsing fail.
*/
protected abstract boolean supportsUnknownFields();
/**
* Returns a predicate that given the field name indicates whether the field has to be excluded from random fields insertion or not
*/
protected Predicate<String> getRandomFieldsExcludeFilter() {
return field -> false;
}
/**
* Fields that have to be ignored when shuffling as part of testFromXContent
*/
protected String[] getShuffleFieldsExceptions() {
return Strings.EMPTY_ARRAY;
}
}

View File

@ -29,7 +29,6 @@ import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import com.carrotsearch.randomizedtesting.generators.RandomStrings;
import com.carrotsearch.randomizedtesting.rules.TestRuleAdapter;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -48,6 +47,7 @@ import org.elasticsearch.bootstrap.BootstrapForTesting;
import org.elasticsearch.client.Requests;
import org.elasticsearch.cluster.ClusterModule;
import org.elasticsearch.cluster.metadata.IndexMetaData;
import org.elasticsearch.common.CheckedBiFunction;
import org.elasticsearch.common.CheckedRunnable;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.PathUtils;
@ -57,6 +57,7 @@ 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 org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
@ -926,8 +927,21 @@ public abstract class ESTestCase extends LuceneTestCase {
*/
protected final BytesReference toShuffledXContent(ToXContent toXContent, XContentType xContentType, ToXContent.Params params,
boolean humanReadable, String... exceptFieldNames) throws IOException{
return toShuffledXContent(toXContent, xContentType, params, humanReadable, this::createParser, exceptFieldNames);
}
/**
* Returns the bytes that represent the XContent output of the provided {@link ToXContent} object, using the provided
* {@link XContentType}. Wraps the output into a new anonymous object according to the value returned
* by the {@link ToXContent#isFragment()} method returns. Shuffles the keys to make sure that parsing never relies on keys ordering.
*/
protected static BytesReference toShuffledXContent(ToXContent toXContent, XContentType xContentType, ToXContent.Params params,
boolean humanReadable,
CheckedBiFunction<XContent, BytesReference, XContentParser, IOException>
parserFunction,
String... exceptFieldNames) throws IOException{
BytesReference bytes = XContentHelper.toXContent(toXContent, xContentType, params, humanReadable);
try (XContentParser parser = createParser(xContentType.xContent(), bytes)) {
try (XContentParser parser = parserFunction.apply(xContentType.xContent(), bytes)) {
try (XContentBuilder builder = shuffleXContent(parser, rarely(), exceptFieldNames)) {
return builder.bytes();
}
@ -952,7 +966,8 @@ public abstract class ESTestCase extends LuceneTestCase {
* recursive shuffling behavior can be made by passing in the names of fields which
* internally should stay untouched.
*/
public XContentBuilder shuffleXContent(XContentParser parser, boolean prettyPrint, String... exceptFieldNames) throws IOException {
public static XContentBuilder shuffleXContent(XContentParser parser, boolean prettyPrint, String... exceptFieldNames)
throws IOException {
XContentBuilder xContentBuilder = XContentFactory.contentBuilder(parser.contentType());
if (prettyPrint) {
xContentBuilder.prettyPrint();
@ -969,6 +984,7 @@ public abstract class ESTestCase extends LuceneTestCase {
}
// shuffle fields of objects in the list, but not the list itself
@SuppressWarnings("unchecked")
private static List<Object> shuffleList(List<Object> list, Set<String> exceptFields) {
List<Object> targetList = new ArrayList<>();
for(Object value : list) {
@ -985,6 +1001,7 @@ public abstract class ESTestCase extends LuceneTestCase {
return targetList;
}
@SuppressWarnings("unchecked")
public static LinkedHashMap<String, Object> shuffleMap(LinkedHashMap<String, Object> map, Set<String> exceptFields) {
List<String> keys = new ArrayList<>(map.keySet());
LinkedHashMap<String, Object> targetMap = new LinkedHashMap<>();
@ -1010,11 +1027,39 @@ public abstract class ESTestCase extends LuceneTestCase {
* potentially need to use a {@link NamedWriteableRegistry}, so this needs to be provided too (although it can be
* empty if the object that is streamed doesn't contain any {@link NamedWriteable} objects itself.
*/
public static <T extends Writeable> T copyWriteable(T original, NamedWriteableRegistry namedWritabelRegistry,
public static <T extends Writeable> T copyWriteable(T original, NamedWriteableRegistry namedWriteableRegistry,
Writeable.Reader<T> reader) throws IOException {
return copyWriteable(original, namedWriteableRegistry, reader, Version.CURRENT);
}
/**
* Same as {@link #copyWriteable(Writeable, NamedWriteableRegistry, Writeable.Reader)} but also allows to provide
* a {@link Version} argument which will be used to write and read back the object.
*/
public static <T extends Writeable> T copyWriteable(T original, NamedWriteableRegistry namedWriteableRegistry,
Writeable.Reader<T> reader, Version version) throws IOException {
return copyInstance(original, namedWriteableRegistry, (out, value) -> value.writeTo(out), reader, version);
}
/**
* Create a copy of an original {@link Streamable} object by running it through a {@link BytesStreamOutput} and
* reading it in again using a provided {@link Writeable.Reader}. The stream that is wrapped around the {@link StreamInput}
* potentially need to use a {@link NamedWriteableRegistry}, so this needs to be provided too (although it can be
* empty if the object that is streamed doesn't contain any {@link NamedWriteable} objects itself.
*/
public static <T extends Streamable> T copyStreamable(T original, NamedWriteableRegistry namedWriteableRegistry,
Supplier<T> supplier, Version version) throws IOException {
return copyInstance(original, namedWriteableRegistry, (out, value) -> value.writeTo(out),
Streamable.newWriteableReader(supplier), version);
}
private static <T> T copyInstance(T original, NamedWriteableRegistry namedWriteableRegistry, Writeable.Writer<T> writer,
Writeable.Reader<T> reader, Version version) throws IOException {
try (BytesStreamOutput output = new BytesStreamOutput()) {
original.writeTo(output);
try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWritabelRegistry)) {
output.setVersion(version);
writer.write(output, original);
try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) {
in.setVersion(version);
return reader.read(in);
}
}