-
Notifications
You must be signed in to change notification settings - Fork 24.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Increase coverage in SearchSortValuesTests #36597
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,108 +19,114 @@ | |
|
||
package org.elasticsearch.search; | ||
|
||
import org.apache.lucene.util.BytesRef; | ||
import org.elasticsearch.Version; | ||
import org.elasticsearch.common.Strings; | ||
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; | ||
import org.elasticsearch.common.lucene.LuceneTests; | ||
import org.elasticsearch.common.xcontent.ToXContent; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.XContentParser; | ||
import org.elasticsearch.common.xcontent.XContentType; | ||
import org.elasticsearch.common.xcontent.json.JsonXContent; | ||
import org.elasticsearch.test.ESTestCase; | ||
import org.elasticsearch.test.AbstractSerializingTestCase; | ||
import org.elasticsearch.test.RandomObjects; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.function.Supplier; | ||
|
||
import static org.elasticsearch.common.xcontent.XContentHelper.toXContent; | ||
import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; | ||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; | ||
|
||
public class SearchSortValuesTests extends ESTestCase { | ||
|
||
public static SearchSortValues createTestItem() { | ||
List<Supplier<Object>> valueSuppliers = new ArrayList<>(); | ||
// this should reflect all values that are allowed to go through the transport layer | ||
valueSuppliers.add(() -> null); | ||
valueSuppliers.add(ESTestCase::randomInt); | ||
valueSuppliers.add(ESTestCase::randomLong); | ||
valueSuppliers.add(ESTestCase::randomDouble); | ||
valueSuppliers.add(ESTestCase::randomFloat); | ||
valueSuppliers.add(ESTestCase::randomByte); | ||
valueSuppliers.add(ESTestCase::randomShort); | ||
valueSuppliers.add(ESTestCase::randomBoolean); | ||
valueSuppliers.add(() -> frequently() ? randomAlphaOfLengthBetween(1, 30) : randomRealisticUnicodeOfCodepointLength(30)); | ||
public class SearchSortValuesTests extends AbstractSerializingTestCase<SearchSortValues> { | ||
|
||
public static SearchSortValues createTestItem(XContentType xContentType, boolean transportSerialization) { | ||
int size = randomIntBetween(1, 20); | ||
Object[] values = new Object[size]; | ||
DocValueFormat[] sortValueFormats = new DocValueFormat[size]; | ||
for (int i = 0; i < size; i++) { | ||
Supplier<Object> supplier = randomFrom(valueSuppliers); | ||
values[i] = supplier.get(); | ||
Object sortValue = randomSortValue(xContentType, transportSerialization); | ||
values[i] = sortValue; | ||
//make sure that for BytesRef, we provide a specific doc value format that overrides format(BytesRef) | ||
sortValueFormats[i] = sortValue instanceof BytesRef ? DocValueFormat.RAW : randomDocValueFormat(); | ||
} | ||
return new SearchSortValues(values); | ||
return new SearchSortValues(values, sortValueFormats); | ||
} | ||
|
||
private static Object randomSortValue(XContentType xContentType, boolean transportSerialization) { | ||
Object randomSortValue = LuceneTests.randomSortValue(); | ||
//to simplify things, we directly serialize what we expect we would parse back when testing xcontent serialization | ||
return transportSerialization ? randomSortValue : RandomObjects.getExpectedParsedValue(xContentType, randomSortValue); | ||
} | ||
|
||
public void testFromXContent() throws IOException { | ||
SearchSortValues sortValues = createTestItem(); | ||
XContentType xcontentType = randomFrom(XContentType.values()); | ||
boolean humanReadable = randomBoolean(); | ||
BytesReference originalBytes = toShuffledXContent(sortValues, xcontentType, ToXContent.EMPTY_PARAMS, humanReadable); | ||
private static DocValueFormat randomDocValueFormat() { | ||
return randomFrom(DocValueFormat.BOOLEAN, DocValueFormat.RAW, DocValueFormat.IP, DocValueFormat.BINARY, DocValueFormat.GEOHASH); | ||
} | ||
|
||
SearchSortValues parsed; | ||
try (XContentParser parser = createParser(xcontentType.xContent(), originalBytes)) { | ||
parser.nextToken(); // skip to the elements start array token, fromXContent advances from there if called | ||
parser.nextToken(); | ||
parser.nextToken(); | ||
parsed = SearchSortValues.fromXContent(parser); | ||
parser.nextToken(); | ||
assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); | ||
assertNull(parser.nextToken()); | ||
} | ||
assertToXContentEquivalent(originalBytes, toXContent(parsed, xcontentType, humanReadable), xcontentType); | ||
@Override | ||
protected SearchSortValues doParseInstance(XContentParser parser) throws IOException { | ||
parser.nextToken(); // skip to the elements start array token, fromXContent advances from there if called | ||
parser.nextToken(); | ||
parser.nextToken(); | ||
SearchSortValues searchSortValues = SearchSortValues.fromXContent(parser); | ||
parser.nextToken(); | ||
assertEquals(XContentParser.Token.END_OBJECT, parser.currentToken()); | ||
assertNull(parser.nextToken()); | ||
return searchSortValues; | ||
} | ||
|
||
public void testToXContent() throws IOException { | ||
SearchSortValues sortValues = new SearchSortValues(new Object[]{ 1, "foo", 3.0}); | ||
XContentBuilder builder = JsonXContent.contentBuilder(); | ||
builder.startObject(); | ||
sortValues.toXContent(builder, ToXContent.EMPTY_PARAMS); | ||
builder.endObject(); | ||
assertEquals("{\"sort\":[1,\"foo\",3.0]}", Strings.toString(builder)); | ||
@Override | ||
protected SearchSortValues createXContextTestInstance(XContentType xContentType) { | ||
return createTestItem(xContentType, false); | ||
} | ||
|
||
/** | ||
* Test equality and hashCode properties | ||
*/ | ||
public void testEqualsAndHashcode() { | ||
checkEqualsAndHashCode(createTestItem(), SearchSortValuesTests::copy, SearchSortValuesTests::mutate); | ||
@Override | ||
protected SearchSortValues createTestInstance() { | ||
return createTestItem(randomFrom(XContentType.values()), true); | ||
} | ||
|
||
public void testSerialization() throws IOException { | ||
SearchSortValues sortValues = createTestItem(); | ||
try (BytesStreamOutput output = new BytesStreamOutput()) { | ||
sortValues.writeTo(output); | ||
try (StreamInput in = output.bytes().streamInput()) { | ||
SearchSortValues deserializedCopy = new SearchSortValues(in); | ||
assertEquals(sortValues, deserializedCopy); | ||
assertEquals(sortValues.hashCode(), deserializedCopy.hashCode()); | ||
assertNotSame(sortValues, deserializedCopy); | ||
} | ||
@Override | ||
protected Writeable.Reader<SearchSortValues> instanceReader() { | ||
return SearchSortValues::new; | ||
} | ||
|
||
@Override | ||
protected String[] getShuffleFieldsExceptions() { | ||
return new String[]{"sort"}; | ||
} | ||
|
||
public void testToXContent() throws IOException { | ||
{ | ||
SearchSortValues sortValues = new SearchSortValues(new Object[]{ 1, "foo", 3.0}); | ||
XContentBuilder builder = JsonXContent.contentBuilder(); | ||
builder.startObject(); | ||
sortValues.toXContent(builder, ToXContent.EMPTY_PARAMS); | ||
builder.endObject(); | ||
assertEquals("{\"sort\":[1,\"foo\",3.0]}", Strings.toString(builder)); | ||
} | ||
{ | ||
SearchSortValues sortValues = new SearchSortValues(new Object[0]); | ||
XContentBuilder builder = JsonXContent.contentBuilder(); | ||
builder.startObject(); | ||
sortValues.toXContent(builder, ToXContent.EMPTY_PARAMS); | ||
builder.endObject(); | ||
assertEquals("{}", Strings.toString(builder)); | ||
} | ||
} | ||
|
||
private static SearchSortValues mutate(SearchSortValues original) { | ||
Object[] sortValues = original.sortValues(); | ||
@Override | ||
protected SearchSortValues mutateInstance(SearchSortValues instance) { | ||
Object[] sortValues = instance.sortValues(); | ||
if (sortValues.length == 0) { | ||
return new SearchSortValues(new Object[] { 1 }); | ||
return createTestInstance(); | ||
} | ||
return new SearchSortValues(Arrays.copyOf(sortValues, sortValues.length + 1)); | ||
if (randomBoolean()) { | ||
return new SearchSortValues(new Object[0]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks like a rare case, what is it for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just checking if equals/hashcode work well in this corner-case too |
||
} | ||
Object[] values = Arrays.copyOf(sortValues, sortValues.length + 1); | ||
values[sortValues.length] = randomSortValue(randomFrom(XContentType.values()), true); | ||
return new SearchSortValues(values); | ||
} | ||
|
||
private static SearchSortValues copy(SearchSortValues original) { | ||
return new SearchSortValues(Arrays.copyOf(original.sortValues(), original.sortValues().length)); | ||
@Override | ||
protected SearchSortValues copyInstance(SearchSortValues instance, Version version) { | ||
return new SearchSortValues(Arrays.copyOf(instance.sortValues(), instance.sortValues().length)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,6 @@ | |
|
||
import com.carrotsearch.randomizedtesting.generators.RandomPicks; | ||
import com.carrotsearch.randomizedtesting.generators.RandomStrings; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.action.support.replication.ReplicationResponse.ShardInfo; | ||
import org.elasticsearch.action.support.replication.ReplicationResponse.ShardInfo.Failure; | ||
|
@@ -72,78 +71,85 @@ private RandomObjects() { | |
*/ | ||
public static Tuple<List<Object>, List<Object>> randomStoredFieldValues(Random random, XContentType xContentType) { | ||
int numValues = randomIntBetween(random, 1, 5); | ||
List<Object> originalValues = new ArrayList<>(); | ||
List<Object> expectedParsedValues = new ArrayList<>(); | ||
List<Object> originalValues = randomStoredFieldValues(random, numValues); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can also use LuceneTests#randomSortValue() here and get rif of the private helper alltogether? I see its slightly different in the way it e.g. draws strings, but then again we also use it in https://github.com/elastic/elasticsearch/pull/36597/files#diff-c5ebccd66a4f939ea845842b8cfea549R54 for the single-value case as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sort values and stored fields are two different things. The differences are mainly around strings/byte arrays indeed. We could consider simplifying things in a follow-up but this PR does not make things worse, I made sure that we share the common bits around parsing and the difference between original and parsed value when they go through xcontent serialization. |
||
List<Object> expectedParsedValues = new ArrayList<>(numValues); | ||
for (Object originalValue : originalValues) { | ||
expectedParsedValues.add(getExpectedParsedValue(xContentType, originalValue)); | ||
} | ||
return Tuple.tuple(originalValues, expectedParsedValues); | ||
} | ||
|
||
private static List<Object> randomStoredFieldValues(Random random, int numValues) { | ||
List<Object> values = new ArrayList<>(numValues); | ||
int dataType = randomIntBetween(random, 0, 8); | ||
for (int i = 0; i < numValues; i++) { | ||
switch(dataType) { | ||
case 0: | ||
long randomLong = random.nextLong(); | ||
originalValues.add(randomLong); | ||
expectedParsedValues.add(randomLong); | ||
values.add(random.nextLong()); | ||
break; | ||
case 1: | ||
int randomInt = random.nextInt(); | ||
originalValues.add(randomInt); | ||
expectedParsedValues.add(randomInt); | ||
values.add(random.nextInt()); | ||
break; | ||
case 2: | ||
Short randomShort = (short) random.nextInt(); | ||
originalValues.add(randomShort); | ||
expectedParsedValues.add(randomShort.intValue()); | ||
values.add((short) random.nextInt()); | ||
break; | ||
case 3: | ||
Byte randomByte = (byte)random.nextInt(); | ||
originalValues.add(randomByte); | ||
expectedParsedValues.add(randomByte.intValue()); | ||
values.add((byte) random.nextInt()); | ||
break; | ||
case 4: | ||
double randomDouble = random.nextDouble(); | ||
originalValues.add(randomDouble); | ||
expectedParsedValues.add(randomDouble); | ||
values.add(random.nextDouble()); | ||
break; | ||
case 5: | ||
Float randomFloat = random.nextFloat(); | ||
originalValues.add(randomFloat); | ||
if (xContentType == XContentType.CBOR) { | ||
//with CBOR we get back a float | ||
expectedParsedValues.add(randomFloat); | ||
} else if (xContentType == XContentType.SMILE) { | ||
//with SMILE we get back a double (this will change in Jackson 2.9 where it will return a Float) | ||
expectedParsedValues.add(randomFloat.doubleValue()); | ||
} else { | ||
//with JSON AND YAML we get back a double, but with float precision. | ||
expectedParsedValues.add(Double.parseDouble(randomFloat.toString())); | ||
} | ||
values.add(random.nextFloat()); | ||
break; | ||
case 6: | ||
boolean randomBoolean = random.nextBoolean(); | ||
originalValues.add(randomBoolean); | ||
expectedParsedValues.add(randomBoolean); | ||
values.add(random.nextBoolean()); | ||
break; | ||
case 7: | ||
String randomString = random.nextBoolean() ? RandomStrings.randomAsciiLettersOfLengthBetween(random, 3, 10) : | ||
randomUnicodeOfLengthBetween(random, 3, 10); | ||
originalValues.add(randomString); | ||
expectedParsedValues.add(randomString); | ||
values.add(random.nextBoolean() ? RandomStrings.randomAsciiLettersOfLengthBetween(random, 3, 10) : | ||
randomUnicodeOfLengthBetween(random, 3, 10)); | ||
break; | ||
case 8: | ||
byte[] randomBytes = RandomStrings.randomUnicodeOfLengthBetween(random, 10, 50).getBytes(StandardCharsets.UTF_8); | ||
BytesArray randomBytesArray = new BytesArray(randomBytes); | ||
originalValues.add(randomBytesArray); | ||
if (xContentType == XContentType.JSON || xContentType == XContentType.YAML) { | ||
//JSON and YAML write the base64 format | ||
expectedParsedValues.add(Base64.getEncoder().encodeToString(randomBytes)); | ||
} else { | ||
//SMILE and CBOR write the original bytes as they support binary format | ||
expectedParsedValues.add(randomBytesArray); | ||
} | ||
values.add(new BytesArray(randomBytes)); | ||
break; | ||
default: | ||
throw new UnsupportedOperationException(); | ||
} | ||
} | ||
return Tuple.tuple(originalValues, expectedParsedValues); | ||
return values; | ||
} | ||
|
||
/** | ||
* Converts the provided field value to its corresponding expected value once printed out | ||
* via {@link org.elasticsearch.common.xcontent.ToXContent#toXContent(XContentBuilder, ToXContent.Params)} and parsed back via | ||
* {@link org.elasticsearch.common.xcontent.XContentParser#objectText()}. | ||
* Generates values based on what can get printed out. Stored fields values are retrieved from lucene and converted via | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: s/lucene/Lucene There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have plenty of references to either lucene or Lucene in our codebase comments. I am not sure that going for one or the other makes things more consistent. |
||
* {@link org.elasticsearch.index.mapper.MappedFieldType#valueForDisplay(Object)} to either strings, numbers or booleans. | ||
*/ | ||
public static Object getExpectedParsedValue(XContentType xContentType, Object value) { | ||
if (value instanceof BytesArray) { | ||
if (xContentType == XContentType.JSON || xContentType == XContentType.YAML) { | ||
//JSON and YAML write the base64 format | ||
return Base64.getEncoder().encodeToString(((BytesArray)value).toBytesRef().bytes); | ||
} | ||
} | ||
if (value instanceof Float) { | ||
if (xContentType == XContentType.SMILE) { | ||
//with SMILE we get back a double (this will change in Jackson 2.9 where it will return a Float) | ||
return ((Float)value).doubleValue(); | ||
} else { | ||
//with JSON AND YAML we get back a double, but with float precision. | ||
return Double.parseDouble(value.toString()); | ||
} | ||
} | ||
if (value instanceof Byte) { | ||
return ((Byte)value).intValue(); | ||
} | ||
if (value instanceof Short) { | ||
return ((Short)value).intValue(); | ||
} | ||
return value; | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity: why is that important? Is it easy enough to explain to add to the comment without making it too long?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the question? What is too long? This is a tricky bit of the sort values, that they get formatted when doc value formats are provided (which happens whenever the class is used on the server-side).