Skip to content

Commit eeb4afb

Browse files
authored
Run core's integration tests with runtime fields (#60931)
Adds the `x-pack/plugin/runtime-fields/qa/rest` project to run core's integration tests against an index with `runtime_scipt` fields. This works by modifying the test configuration on load to replace supported field types with `runtime_script`. Relates to #59332
1 parent fd8b557 commit eeb4afb

File tree

4 files changed

+269
-31
lines changed

4 files changed

+269
-31
lines changed

rest-api-spec/src/main/resources/rest-api-spec/test/search.aggregation/20_terms.yml

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ setup:
724724
body: { "size" : 0, "aggs" : { "no_field_terms" : { "terms" : { "size": 1 } } } }
725725

726726
---
727-
"string profiler":
727+
"string profiler via global ordinals":
728728
- skip:
729729
version: " - 7.8.99"
730730
reason: debug information added in 7.9.0
@@ -776,36 +776,6 @@ setup:
776776
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
777777
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
778778

779-
- do:
780-
search:
781-
index: test_1
782-
body:
783-
profile: true
784-
size: 0
785-
aggs:
786-
str_terms:
787-
terms:
788-
field: str
789-
collect_mode: breadth_first
790-
execution_hint: map
791-
aggs:
792-
max_number:
793-
max:
794-
field: number
795-
- match: { aggregations.str_terms.buckets.0.key: sheep }
796-
- match: { aggregations.str_terms.buckets.0.max_number.value: 3 }
797-
- match: { aggregations.str_terms.buckets.1.key: cow }
798-
- match: { aggregations.str_terms.buckets.1.max_number.value: 1 }
799-
- match: { aggregations.str_terms.buckets.2.key: pig }
800-
- match: { aggregations.str_terms.buckets.2.max_number.value: 1 }
801-
- match: { profile.shards.0.aggregations.0.type: MapStringTermsAggregator }
802-
- match: { profile.shards.0.aggregations.0.description: str_terms }
803-
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 4 }
804-
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
805-
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
806-
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
807-
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
808-
809779
- do:
810780
indices.create:
811781
index: test_3
@@ -857,6 +827,55 @@ setup:
857827
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
858828
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
859829

830+
---
831+
"string profiler via map":
832+
- skip:
833+
version: " - 7.8.99"
834+
reason: debug information added in 7.9.0
835+
- do:
836+
bulk:
837+
index: test_1
838+
refresh: true
839+
body: |
840+
{ "index": {} }
841+
{ "str": "sheep", "number": 1 }
842+
{ "index": {} }
843+
{ "str": "sheep", "number": 3 }
844+
{ "index": {} }
845+
{ "str": "cow", "number": 1 }
846+
{ "index": {} }
847+
{ "str": "pig", "number": 1 }
848+
849+
- do:
850+
search:
851+
index: test_1
852+
body:
853+
profile: true
854+
size: 0
855+
aggs:
856+
str_terms:
857+
terms:
858+
field: str
859+
collect_mode: breadth_first
860+
execution_hint: map
861+
aggs:
862+
max_number:
863+
max:
864+
field: number
865+
- match: { aggregations.str_terms.buckets.0.key: sheep }
866+
- match: { aggregations.str_terms.buckets.0.max_number.value: 3 }
867+
- match: { aggregations.str_terms.buckets.1.key: cow }
868+
- match: { aggregations.str_terms.buckets.1.max_number.value: 1 }
869+
- match: { aggregations.str_terms.buckets.2.key: pig }
870+
- match: { aggregations.str_terms.buckets.2.max_number.value: 1 }
871+
- match: { profile.shards.0.aggregations.0.type: MapStringTermsAggregator }
872+
- match: { profile.shards.0.aggregations.0.description: str_terms }
873+
- match: { profile.shards.0.aggregations.0.breakdown.collect_count: 4 }
874+
- match: { profile.shards.0.aggregations.0.debug.deferred_aggregators: [ max_number ] }
875+
- match: { profile.shards.0.aggregations.0.debug.result_strategy: terms }
876+
- match: { profile.shards.0.aggregations.0.children.0.type: MaxAggregator }
877+
- match: { profile.shards.0.aggregations.0.children.0.description: max_number }
878+
860879
---
861880
"numeric profiler":
862881
- skip:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Empty project so we can pick up its subproject
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
apply plugin: 'elasticsearch.yaml-rest-test'
2+
3+
restResources {
4+
restTests {
5+
includeCore '*'
6+
}
7+
}
8+
9+
testClusters.yamlRestTest {
10+
testDistribution = 'DEFAULT'
11+
}
12+
13+
yamlRestTest {
14+
systemProperty 'tests.rest.suite',
15+
[
16+
'search',
17+
'search.aggregation',
18+
'search.highlight',
19+
'search.inner_hits',
20+
'search_shards',
21+
'suggest',
22+
'msearch',
23+
'field_caps',
24+
].join(',')
25+
systemProperty 'tests.rest.blacklist',
26+
[
27+
/////// TO FIX ///////
28+
'search/330_fetch_fields/*', // The whole API is not yet supported
29+
'search.aggregation/20_terms/Global ordinals are not loaded with the map execution hint', // Broken. Gotta fix.
30+
'search.highlight/40_keyword_ignore/Plain Highligher should skip highlighting ignored keyword values', // Broken. Gotta fix.
31+
'search/115_multiple_field_collapsing/two levels fields collapsing', // Broken. Gotta fix.
32+
'search/140_pre_filter_search_shards/pre_filter_shard_size with shards that have no hit', // Broken. Gotta fix.
33+
'field_caps/30_filter/Field caps with index filter', // We don't support filtering field caps on runtime fields. What should we do?
34+
'search.aggregation/10_histogram/*', // runtime_script doesn't support sub-fields. Maybe it should?
35+
/////// TO FIX ///////
36+
37+
/////// NOT SUPPORTED ///////
38+
'search.aggregation/280_rare_terms/*', // Requires an index and we won't have it
39+
'search.aggregation/20_terms/string profiler via global ordinals', // Runtime fields don't have global ords
40+
/////// NOT SUPPORTED ///////
41+
].join(',')
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.runtimefields.rest;
8+
9+
import com.carrotsearch.randomizedtesting.annotations.Name;
10+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
11+
12+
import org.elasticsearch.index.mapper.IpFieldMapper;
13+
import org.elasticsearch.index.mapper.KeywordFieldMapper;
14+
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
15+
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
16+
import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
17+
import org.elasticsearch.test.rest.yaml.section.DoSection;
18+
import org.elasticsearch.test.rest.yaml.section.ExecutableSection;
19+
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
26+
public class CoreTestsWithRuntimeFieldsIT extends ESClientYamlSuiteTestCase {
27+
public CoreTestsWithRuntimeFieldsIT(@Name("yaml") ClientYamlTestCandidate testCandidate) {
28+
super(testCandidate);
29+
}
30+
31+
/**
32+
* Builds test parameters similarly to {@link ESClientYamlSuiteTestCase#createParameters()},
33+
* replacing the body of index creation commands so that fields are {@code runtime_script}s
34+
* that load from {@code source} instead of their original type. Test configurations that
35+
* do are not modified to contain runtime fields are not returned as they are tested
36+
* elsewhere.
37+
*/
38+
@ParametersFactory
39+
public static Iterable<Object[]> parameters() throws Exception {
40+
/*
41+
* Map of "setup"s that we've seen - from path to whether or
42+
* not we the setup was modified to include a runtime_script
43+
*/
44+
Map<String, Boolean> seenSetups = new HashMap<>();
45+
List<Object[]> result = new ArrayList<>();
46+
for (Object[] orig : ESClientYamlSuiteTestCase.createParameters()) {
47+
assert orig.length == 1;
48+
ClientYamlTestCandidate candidate = (ClientYamlTestCandidate) orig[0];
49+
boolean modifiedSetup = seenSetups.computeIfAbsent(
50+
candidate.getName(),
51+
k -> modifySection(candidate.getSuitePath() + "/setup", candidate.getSetupSection().getExecutableSections())
52+
);
53+
boolean modifiedTest = modifySection(candidate.getTestPath(), candidate.getTestSection().getExecutableSections());
54+
if (modifiedSetup || modifiedTest) {
55+
result.add(new Object[] { candidate });
56+
}
57+
}
58+
return result;
59+
}
60+
61+
/**
62+
* Replace property configuration in {@code indices.create} with scripts
63+
* that load from the source.
64+
* @return {@code true} if any fields were rewritten into runtime_scripts, {@code false} otherwise.
65+
*/
66+
private static boolean modifySection(String sectionName, List<ExecutableSection> executables) {
67+
boolean include = false;
68+
for (ExecutableSection section : executables) {
69+
if (false == (section instanceof DoSection)) {
70+
continue;
71+
}
72+
DoSection doSection = (DoSection) section;
73+
if (false == doSection.getApiCallSection().getApi().equals("indices.create")) {
74+
continue;
75+
}
76+
for (Map<?, ?> body : doSection.getApiCallSection().getBodies()) {
77+
Object settings = body.get("settings");
78+
if (settings instanceof Map && ((Map<?, ?>) settings).containsKey("sort.field")) {
79+
/*
80+
* You can't sort the index on a runtime_keyword and it is
81+
* hard to figure out if the sort was a runtime_keyword so
82+
* let's just skip this test.
83+
*/
84+
continue;
85+
}
86+
Object mappings = body.get("mappings");
87+
if (false == (mappings instanceof Map)) {
88+
continue;
89+
}
90+
Object properties = ((Map<?, ?>) mappings).get("properties");
91+
if (false == (properties instanceof Map)) {
92+
continue;
93+
}
94+
for (Map.Entry<?, ?> property : ((Map<?, ?>) properties).entrySet()) {
95+
if (false == property.getValue() instanceof Map) {
96+
continue;
97+
}
98+
@SuppressWarnings("unchecked")
99+
Map<String, Object> propertyMap = (Map<String, Object>) property.getValue();
100+
String name = property.getKey().toString();
101+
String type = Objects.toString(propertyMap.get("type"));
102+
if ("false".equals(Objects.toString(propertyMap.get("doc_values")))) {
103+
// If doc_values is false we can't emulate with scripts. `null` and `true` are fine.
104+
continue;
105+
}
106+
if ("false".equals(Objects.toString(propertyMap.get("index")))) {
107+
// If index is false we can't emulate with scripts
108+
continue;
109+
}
110+
if ("true".equals(Objects.toString(propertyMap.get("store")))) {
111+
// If store is true we can't emulate with scripts
112+
continue;
113+
}
114+
if (propertyMap.containsKey("ignore_above")) {
115+
// Scripts don't support ignore_above so we skip those fields
116+
continue;
117+
}
118+
if (propertyMap.containsKey("ignore_malformed")) {
119+
// Our source reading script doesn't emulate ignore_malformed
120+
continue;
121+
}
122+
String toLoad = painlessToLoadFromSource(name, type);
123+
if (toLoad == null) {
124+
continue;
125+
}
126+
propertyMap.put("type", "runtime_script");
127+
propertyMap.put("runtime_type", type);
128+
propertyMap.put("script", toLoad);
129+
propertyMap.remove("store");
130+
propertyMap.remove("index");
131+
propertyMap.remove("doc_values");
132+
include = true;
133+
}
134+
}
135+
}
136+
return include;
137+
}
138+
139+
private static String painlessToLoadFromSource(String name, String type) {
140+
String emit = PAINLESS_TO_EMIT.get(type);
141+
if (emit == null) {
142+
return null;
143+
}
144+
StringBuilder b = new StringBuilder();
145+
b.append("def v = source['").append(name).append("'];\n");
146+
b.append("if (v instanceof Iterable) {\n");
147+
b.append(" for (def vv : ((Iterable) v)) {\n");
148+
b.append(" if (vv != null) {\n");
149+
b.append(" def value = vv;\n");
150+
b.append(" ").append(emit).append("\n");
151+
b.append(" }\n");
152+
b.append(" }\n");
153+
b.append("} else {\n");
154+
b.append(" if (v != null) {\n");
155+
b.append(" def value = v;\n");
156+
b.append(" ").append(emit).append("\n");
157+
b.append(" }\n");
158+
b.append("}\n");
159+
return b.toString();
160+
}
161+
162+
private static final Map<String, String> PAINLESS_TO_EMIT = Map.ofEntries(
163+
// TODO implement dates against the parser
164+
Map.entry(
165+
NumberType.DOUBLE.typeName(),
166+
"value(value instanceof Number ? ((Number) value).doubleValue() : Double.parseDouble(value.toString()));"
167+
),
168+
Map.entry(KeywordFieldMapper.CONTENT_TYPE, "value(value.toString());"),
169+
Map.entry(IpFieldMapper.CONTENT_TYPE, "stringValue(value.toString());"),
170+
Map.entry(
171+
NumberType.LONG.typeName(),
172+
"value(value instanceof Number ? ((Number) value).longValue() : Long.parseLong(value.toString()));"
173+
)
174+
);
175+
176+
}

0 commit comments

Comments
 (0)