Bladeren bron

REST tests: Introduce add/remove/replace "match" assertions for compatibilty (#68338)

This commit introduces the capability to add/remove/replace "match" assertions. 
This is used for REST API compatibility testing the YAML REST tests are sourced 
from N-1 version and executed against a N version cluster. 

A previous commit introduced RestCompatTestTransformTask which allows the YAML 
REST tests used for REST API compatibility testing to be changed (transformed) 
before they are executed. This commit builds on that to allow per test 
(within a project), or all tests within a project to be configured to transform 
the "match" assertion for the REST tests. 

The configuration is specified via RestCompatTestTransformTask in build.gradle, 
for example:
```
tasks.named("transformV7RestTests").configure({ task ->
  task.replaceMatch("_type", "_doc")
  task.replaceMatch("_source.values", ["z", "x", "y"], "test_one")
  task.removeMatch("_source.blah")
  task.removeMatch("_source.junk", "test_two")
  task.addMatch("_source.added", [name: 'jake', likes: 'cheese'], "test_one")
})
```

The task name has been changed to include the version to help ensure that the transforms 
only run against the expected version. For example, when master becomes v9, then the task 
name will be `transformV8RestTests`and the v7 will hopefully be obvious is no longer 
intended to run (since that task won't even exist anymore).
Jake Landis 4 jaren geleden
bovenliggende
commit
83b168dc0a
22 gewijzigde bestanden met toevoegingen van 1360 en 45 verwijderingen
  1. 150 6
      buildSrc/src/integTest/groovy/org/elasticsearch/gradle/YamlRestCompatTestPluginFuncTest.groovy
  2. 78 8
      buildSrc/src/main/java/org/elasticsearch/gradle/internal/rest/compat/RestCompatTestTransformTask.java
  3. 4 5
      buildSrc/src/main/java/org/elasticsearch/gradle/internal/rest/compat/YamlRestCompatTestPlugin.java
  4. 25 0
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestContext.java
  5. 19 4
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransform.java
  6. 24 0
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformByParentArray.java
  7. 10 1
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformByParentObject.java
  8. 53 13
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformer.java
  9. 12 2
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/headers/InjectHeaders.java
  10. 71 0
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/AddMatch.java
  11. 66 0
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/RemoveMatch.java
  12. 75 0
      buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/ReplaceMatch.java
  13. 8 6
      buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/header/InjectHeaderTests.java
  14. 175 0
      buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/AddMatchTests.java
  15. 209 0
      buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/RemoveMatchTests.java
  16. 200 0
      buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/ReplaceMatchTests.java
  17. 0 0
      buildSrc/src/test/resources/rest/transform/header/no_setup.yml
  18. 0 0
      buildSrc/src/test/resources/rest/transform/header/with_features.yml
  19. 0 0
      buildSrc/src/test/resources/rest/transform/header/with_headers.yml
  20. 0 0
      buildSrc/src/test/resources/rest/transform/header/with_setup.yml
  21. 0 0
      buildSrc/src/test/resources/rest/transform/header/with_skip.yml
  22. 181 0
      buildSrc/src/test/resources/rest/transform/match/match.yml

+ 150 - 6
buildSrc/src/integTest/groovy/org/elasticsearch/gradle/YamlRestCompatTestPluginFuncTest.groovy

@@ -8,13 +8,24 @@
 
 package org.elasticsearch.gradle
 
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.ObjectReader
+import com.fasterxml.jackson.databind.ObjectWriter
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
 import org.elasticsearch.gradle.fixtures.AbstractRestResourcesFuncTest
 import org.elasticsearch.gradle.internal.rest.compat.YamlRestCompatTestPlugin
 import org.gradle.testkit.runner.TaskOutcome
 
 class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
 
-    def intermediateDir = YamlRestCompatTestPlugin.TEST_INTERMEDIATE_DIR_NAME;
+    private static final String intermediateDir = YamlRestCompatTestPlugin.TEST_INTERMEDIATE_DIR_NAME
+    private static final String transformTask  = ":" + YamlRestCompatTestPlugin.TRANSFORM_TASK_NAME
+    private static final YAMLFactory YAML_FACTORY = new YAMLFactory()
+    private static final ObjectMapper MAPPER = new ObjectMapper(YAML_FACTORY)
+    private static final ObjectReader READER = MAPPER.readerFor(ObjectNode.class)
+    private static final ObjectWriter WRITER = MAPPER.writerFor(ObjectNode.class)
+
 
     def "yamlRestCompatTest does nothing when there are no tests"() {
         given:
@@ -39,7 +50,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
         result.task(':yamlRestCompatTest').outcome == TaskOutcome.NO_SOURCE
         result.task(':copyRestCompatApiTask').outcome == TaskOutcome.NO_SOURCE
         result.task(':copyRestCompatTestTask').outcome == TaskOutcome.NO_SOURCE
-        result.task(':transformCompatTests').outcome == TaskOutcome.NO_SOURCE
+        result.task(transformTask).outcome == TaskOutcome.NO_SOURCE
     }
 
     def "yamlRestCompatTest executes and copies api and transforms tests from :bwc:minor"() {
@@ -91,7 +102,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
         result.task(':yamlRestCompatTest').outcome == TaskOutcome.SKIPPED
         result.task(':copyRestCompatApiTask').outcome == TaskOutcome.SUCCESS
         result.task(':copyRestCompatTestTask').outcome == TaskOutcome.SUCCESS
-        result.task(':transformCompatTests').outcome == TaskOutcome.SUCCESS
+        result.task(transformTask).outcome == TaskOutcome.SUCCESS
 
         file("/build/resources/yamlRestCompatTest/rest-api-spec/api/" + api).exists()
         file("/build/resources/yamlRestCompatTest/rest-api-spec/test/" + test).exists()
@@ -119,7 +130,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
         result.task(':yamlRestCompatTest').outcome == TaskOutcome.SKIPPED
         result.task(':copyRestCompatApiTask').outcome == TaskOutcome.UP_TO_DATE
         result.task(':copyRestCompatTestTask').outcome == TaskOutcome.UP_TO_DATE
-        result.task(':transformCompatTests').outcome == TaskOutcome.UP_TO_DATE
+        result.task(transformTask).outcome == TaskOutcome.UP_TO_DATE
     }
 
     def "yamlRestCompatTest is wired into check and checkRestCompat"() {
@@ -148,7 +159,7 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
         result.task(':yamlRestCompatTest').outcome == TaskOutcome.NO_SOURCE
         result.task(':copyRestCompatApiTask').outcome == TaskOutcome.NO_SOURCE
         result.task(':copyRestCompatTestTask').outcome == TaskOutcome.NO_SOURCE
-        result.task(':transformCompatTests').outcome == TaskOutcome.NO_SOURCE
+        result.task(transformTask).outcome == TaskOutcome.NO_SOURCE
 
         when:
         buildFile << """
@@ -162,7 +173,140 @@ class YamlRestCompatTestPluginFuncTest extends AbstractRestResourcesFuncTest {
         result.task(':yamlRestCompatTest').outcome == TaskOutcome.SKIPPED
         result.task(':copyRestCompatApiTask').outcome == TaskOutcome.SKIPPED
         result.task(':copyRestCompatTestTask').outcome == TaskOutcome.SKIPPED
-        result.task(':transformCompatTests').outcome == TaskOutcome.SKIPPED
+        result.task(transformTask).outcome == TaskOutcome.SKIPPED
+    }
+
+    def "transform task executes and works as configured"() {
+        given:
+        internalBuild()
+
+        addSubProject(":distribution:bwc:minor") << """
+        configurations { checkout }
+        artifacts {
+            checkout(new File(projectDir, "checkoutDir"))
+        }
+        """
+
+        buildFile << """
+            apply plugin: 'elasticsearch.yaml-rest-compat-test'
+
+            // avoids a dependency problem in this test, the distribution in use here is inconsequential to the test
+            import org.elasticsearch.gradle.testclusters.TestDistribution;
+            testClusters {
+              yamlRestCompatTest.setTestDistribution(TestDistribution.INTEG_TEST)
+            }
+
+            dependencies {
+               yamlRestTestImplementation "junit:junit:4.12"
+            }
+            tasks.named("transformV7RestTests").configure({ task ->
+              task.replaceMatch("_type", "_doc")
+              task.replaceMatch("_source.values", ["z", "x", "y"], "one")
+              task.removeMatch("_source.blah")
+              task.removeMatch("_source.junk", "two")
+              task.addMatch("_source.added", [name: 'jake', likes: 'cheese'], "one")
+            })
+            // can't actually spin up test cluster from this test
+           tasks.withType(Test).configureEach{ enabled = false }
+        """
+
+        setupRestResources([], [])
+
+        file("distribution/bwc/minor/checkoutDir/src/yamlRestTest/resources/rest-api-spec/test/test.yml" ) << """
+        "one":
+          - do:
+              get:
+                index: test
+                id: 1
+          - match: { _source.values: ["foo"] }
+          - match: { _type: "_foo" }
+          - match: { _source.blah: 1234 }
+          - match: { _source.junk: true }
+        ---
+        "two":
+          - do:
+              get:
+                index: test
+                id: 1
+          - match: { _source.values: ["foo"] }
+          - match: { _type: "_foo" }
+          - match: { _source.blah: 1234 }
+          - match: { _source.junk: true }
+
+        """.stripIndent()
+        when:
+        def result = gradleRunner("yamlRestCompatTest").build()
+
+        then:
+
+        result.task(transformTask).outcome == TaskOutcome.SUCCESS
+
 
+        file("/build/resources/yamlRestCompatTest/rest-api-spec/test/test.yml" ).exists()
+        List<ObjectNode> actual = READER.readValues(file("/build/resources/yamlRestCompatTest/rest-api-spec/test/test.yml")).readAll()
+        List<ObjectNode> expectedAll = READER.readValues(
+        """
+        ---
+        setup:
+        - skip:
+            features: "headers"
+        ---
+        one:
+        - do:
+            get:
+              index: "test"
+              id: 1
+            headers:
+              Content-Type: "application/vnd.elasticsearch+json;compatible-with=7"
+              Accept: "application/vnd.elasticsearch+json;compatible-with=7"
+        - match:
+            _source.values:
+            - "z"
+            - "x"
+            - "y"
+        - match:
+            _type: "_doc"
+        - match: {}
+        - match:
+            _source.junk: true
+        - match:
+            _source.added:
+              name: "jake"
+              likes: "cheese"
+        ---
+        two:
+        - do:
+            get:
+              index: "test"
+              id: 1
+            headers:
+              Content-Type: "application/vnd.elasticsearch+json;compatible-with=7"
+              Accept: "application/vnd.elasticsearch+json;compatible-with=7"
+        - match:
+            _source.values:
+            - "foo"
+        - match:
+            _type: "_doc"
+        - match: {}
+        - match: {}
+        """.stripIndent()).readAll()
+
+        expectedAll.eachWithIndex{ ObjectNode expected, int i ->
+           assert expected == actual.get(i)
+        }
+
+        when:
+        result = gradleRunner(transformTask).build()
+
+        then:
+        result.task(transformTask).outcome == TaskOutcome.UP_TO_DATE
+
+        when:
+        buildFile.write(buildFile.text.replace("blah", "baz"))
+        result = gradleRunner(transformTask).build()
+
+        then:
+        result.task(transformTask).outcome == TaskOutcome.SUCCESS
     }
+
 }

+ 78 - 8
buildSrc/src/main/java/org/elasticsearch/gradle/internal/rest/compat/RestCompatTestTransformTask.java

@@ -8,6 +8,7 @@
 
 package org.elasticsearch.gradle.internal.rest.compat;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import com.fasterxml.jackson.databind.ObjectWriter;
@@ -15,13 +16,17 @@ import com.fasterxml.jackson.databind.SequenceWriter;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
-import org.elasticsearch.gradle.test.rest.transform.InjectHeaders;
 import org.elasticsearch.gradle.test.rest.transform.RestTestTransform;
 import org.elasticsearch.gradle.test.rest.transform.RestTestTransformer;
+import org.elasticsearch.gradle.test.rest.transform.headers.InjectHeaders;
+import org.elasticsearch.gradle.test.rest.transform.match.AddMatch;
+import org.elasticsearch.gradle.test.rest.transform.match.RemoveMatch;
+import org.elasticsearch.gradle.test.rest.transform.match.ReplaceMatch;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.file.FileCollection;
 import org.gradle.api.file.FileTree;
 import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.Nested;
 import org.gradle.api.tasks.OutputDirectory;
 import org.gradle.api.tasks.SkipWhenEmpty;
 import org.gradle.api.tasks.TaskAction;
@@ -32,37 +37,97 @@ import org.gradle.internal.Factory;
 import javax.inject.Inject;
 import java.io.File;
 import java.io.IOException;
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
+import static org.elasticsearch.gradle.internal.rest.compat.YamlRestCompatTestPlugin.COMPATIBLE_VERSION;
+
+/**
+ * A task to transform REST tests for use in REST API compatibility before they are executed.
+ */
 public class RestCompatTestTransformTask extends DefaultTask {
 
     private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
     private static final ObjectMapper MAPPER = new ObjectMapper(YAML_FACTORY);
     private static final ObjectReader READER = MAPPER.readerFor(ObjectNode.class);
     private static final ObjectWriter WRITER = MAPPER.writerFor(ObjectNode.class);
+    private static final String REST_TEST_PREFIX = "rest-api-spec/test";
 
     private static final Map<String, String> headers = Map.of(
         "Content-Type",
-        "application/vnd.elasticsearch+json;compatible-with=7",
+        "application/vnd.elasticsearch+json;compatible-with=" + COMPATIBLE_VERSION,
         "Accept",
-        "application/vnd.elasticsearch+json;compatible-with=7"
+        "application/vnd.elasticsearch+json;compatible-with=" + COMPATIBLE_VERSION
     );
 
     private FileCollection input;
     private File output;
-    private static final String REST_TEST_PREFIX = "rest-api-spec/test";
-
     private final PatternFilterable testPatternSet;
-    private final List<RestTestTransform<?>> transformations;
+    private final List<RestTestTransform<?>> transformations = new ArrayList<>();
 
     @Inject
     public RestCompatTestTransformTask(Factory<PatternSet> patternSetFactory) {
         this.testPatternSet = patternSetFactory.create();
         this.testPatternSet.include("/*" + "*/*.yml"); // concat these strings to keep build from thinking this is invalid javadoc
-        transformations = Collections.singletonList(new InjectHeaders(headers));
+        // always inject compat headers
+        transformations.add(new InjectHeaders(headers));
+    }
+
+    /**
+     * Replaces all the values of a match assertion all project REST tests. For example "match":{"_type": "foo"} to "match":{"_type": "bar"}
+     *
+     * @param subKey the key name directly under match to replace. For example "_type"
+     * @param value  the value used in the replacement. For example "bar"
+     */
+    public void replaceMatch(String subKey, Object value) {
+        transformations.add(new ReplaceMatch(subKey, MAPPER.convertValue(value, JsonNode.class)));
+    }
+
+    /**
+     * Replaces the values of a match assertion for the given REST test. For example "match":{"_type": "foo"} to "match":{"_type": "bar"}
+     *
+     * @param subKey   the key name directly under match to replace. For example "_type"
+     * @param value    the value used in the replacement. For example "bar"
+     * @param testName the testName to apply replacement
+     */
+    public void replaceMatch(String subKey, Object value, String testName) {
+        transformations.add(new ReplaceMatch(subKey, MAPPER.convertValue(value, JsonNode.class), testName));
+    }
+
+    /**
+     * Removes the key/value of a match assertion all project REST tests for the matching subkey.
+     * For example "match":{"_type": "foo"} to "match":{}
+     * An empty match is retained if there is only a single key under match.
+     *
+     * @param subKey the key name directly under match to replace. For example "_type"
+     */
+    public void removeMatch(String subKey) {
+        transformations.add(new RemoveMatch(subKey));
+    }
+
+    /**
+     * Removes the key/value of a match assertion for the given REST tests for the matching subkey.
+     * For example "match":{"_type": "foo"} to "match":{}
+     * An empty match is retained if there is only a single key under match.
+     *
+     * @param subKey   the key name directly under match to remove. For example "_type"
+     * @param testName the testName to apply removal
+     */
+    public void removeMatch(String subKey, String testName) {
+        transformations.add(new RemoveMatch(subKey, testName));
+    }
+
+    /**
+     * Adds a match assertion for the given REST test. For example add "match":{"_type": "foo"} to the test.
+     *
+     * @param subKey   the key name directly under match to add. For example "_type"
+     * @param value    the value used in the addition. For example "foo"
+     * @param testName the testName to apply addition
+     */
+    public void addMatch(String subKey, Object value, String testName) {
+        transformations.add(new AddMatch(subKey, MAPPER.convertValue(value, JsonNode.class), testName));
     }
 
     @OutputDirectory
@@ -98,6 +163,11 @@ public class RestCompatTestTransformTask extends DefaultTask {
         }
     }
 
+    @Nested
+    public List<RestTestTransform<?>> getTransformations() {
+        return transformations;
+    }
+
     public void setInput(FileCollection input) {
         this.input = input;
     }

+ 4 - 5
buildSrc/src/main/java/org/elasticsearch/gradle/internal/rest/compat/YamlRestCompatTestPlugin.java

@@ -46,6 +46,9 @@ import static org.elasticsearch.gradle.test.rest.RestTestUtil.setupDependencies;
  */
 public class YamlRestCompatTestPlugin implements Plugin<Project> {
 
+    public static final int COMPATIBLE_VERSION = Version.fromString(VersionProperties.getVersions().get("elasticsearch")).getMajor() - 1;
+    public static final String TEST_INTERMEDIATE_DIR_NAME = "v" + COMPATIBLE_VERSION + "restTests";
+    public static final String TRANSFORM_TASK_NAME = "transformV" + COMPATIBLE_VERSION + "RestTests";
     public static final String REST_COMPAT_CHECK_TASK_NAME = "checkRestCompat";
     public static final String SOURCE_SET_NAME = "yamlRestCompatTest";
     private static final Path RELATIVE_API_PATH = Path.of("rest-api-spec/api");
@@ -53,9 +56,6 @@ public class YamlRestCompatTestPlugin implements Plugin<Project> {
     private static final Path RELATIVE_REST_API_RESOURCES = Path.of("rest-api-spec/src/main/resources");
     private static final Path RELATIVE_REST_XPACK_RESOURCES = Path.of("x-pack/plugin/src/test/resources");
     private static final Path RELATIVE_REST_PROJECT_RESOURCES = Path.of("src/yamlRestTest/resources");
-    private static final String TEST_INTERMEDIATE_DIR_NAME = "v"
-        + (Version.fromString(VersionProperties.getVersions().get("elasticsearch")).getMajor() - 1)
-        + "restTests";
 
     @Override
     public void apply(Project project) {
@@ -154,14 +154,13 @@ public class YamlRestCompatTestPlugin implements Plugin<Project> {
 
         // transform the copied tests task
         TaskProvider<RestCompatTestTransformTask> transformCompatTestTask = project.getTasks()
-            .register("transformCompatTests", RestCompatTestTransformTask.class, task -> {
+            .register(TRANSFORM_TASK_NAME, RestCompatTestTransformTask.class, task -> {
                 task.dependsOn(copyCompatYamlTestTask);
                 task.dependsOn(yamlCompatTestSourceSet.getProcessResourcesTaskName());
                 File resourceDir = yamlCompatTestSourceSet.getOutput().getResourcesDir();
                 File intermediateDir = new File(resourceDir, TEST_INTERMEDIATE_DIR_NAME);
                 task.setInput(project.files(new File(intermediateDir, RELATIVE_TEST_PATH.toString())));
                 task.setOutput(new File(resourceDir, RELATIVE_TEST_PATH.toString()));
-
                 task.onlyIf(t -> isEnabled(project));
             });
 

+ 25 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestContext.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform;
+
+/**
+ * A place to stash information about a test that is being transformed.
+ */
+public class RestTestContext {
+
+    private final String testName;
+
+    public RestTestContext(String testName) {
+        this.testName = testName;
+    }
+
+    public String getTestName() {
+        return testName;
+    }
+}

+ 19 - 4
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransform.java

@@ -9,17 +9,32 @@
 package org.elasticsearch.gradle.test.rest.transform;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.gradle.api.Named;
+import org.gradle.api.tasks.Input;
 
 /**
  * A single instruction to transforms a REST test.
  */
-public interface RestTestTransform<T extends JsonNode> {
+public interface RestTestTransform<T extends JsonNode> extends Named {
 
     /**
      * Transform the Json structure per the given {@link RestTestTransform}
-     * Implementations are expected to mutate the node (and/or it's parent) to satisfy the transformation.
+     * Implementations are expected to mutate the parent to satisfy the transformation.
      *
-     * @param node The node to transform. This may also be the logical parent of the node that should be transformed.
+     * @param parent The parent of the node to transform.
      */
-    void transformTest(T node);
+    void transformTest(T parent);
+
+    /**
+     * @return true if the transformation should be applied, false otherwise.
+     */
+    default boolean shouldApply(RestTestContext testContext) {
+        return true;
+    }
+
+    @Override
+    @Input
+    default String getName() {
+        return this.getClass().getCanonicalName();
+    }
 }

+ 24 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformByParentArray.java

@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+/**
+ * A type of {@link RestTestTransform} that finds the transformation by a given key that has a value that is an {@link ArrayNode}.
+ */
+public interface RestTestTransformByParentArray extends RestTestTransform<ArrayNode> {
+
+    /**
+     * Arrays are always the value in a key/value pair. Find a key with this name that has an array as the value to identify which Array(s)
+     * to transform.
+     * @return The name of key to find in the REST test that has a value that is an Array
+     */
+    String getKeyOfArrayToFind();
+}

+ 10 - 1
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformByObjectKey.java → buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformByParentObject.java

@@ -13,9 +13,18 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 /**
  * A type of {@link RestTestTransform} that finds the transformation by a given key in to an {@link ObjectNode}.
  */
-public interface RestTestTransformByObjectKey extends RestTestTransform<ObjectNode> {
+public interface RestTestTransformByParentObject extends RestTestTransform<ObjectNode> {
+
     /**
      * @return The name of key to find in the REST test
      */
     String getKeyToFind();
+
+    /**
+     * @return If the value of the ObjectNode is also an ObjectNode, ensure that child key name is also satisfied.
+     * {@code null} to indicate no required children.
+     */
+    default String requiredChildKey() {
+        return null;
+    }
 }

+ 53 - 13
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/RestTestTransformer.java

@@ -9,6 +9,7 @@
 package org.elasticsearch.gradle.test.rest.transform;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
 import java.util.Iterator;
@@ -45,10 +46,16 @@ public class RestTestTransformer {
             .collect(Collectors.toList());
 
         // Collect any transformations that are identified by an object key.
-        Map<String, RestTestTransformByObjectKey> objectKeyFinders = transformations.stream()
-            .filter(transform -> transform instanceof RestTestTransformByObjectKey)
-            .map(transform -> (RestTestTransformByObjectKey) transform)
-            .collect(Collectors.toMap(RestTestTransformByObjectKey::getKeyToFind, transform -> transform));
+        Map<String, List<RestTestTransformByParentObject>> objectKeyFinders = transformations.stream()
+            .filter(transform -> transform instanceof RestTestTransformByParentObject)
+            .map(transform -> (RestTestTransformByParentObject) transform)
+            .collect(Collectors.groupingBy(RestTestTransformByParentObject::getKeyToFind));
+
+        // Collect any transformations that are identified by an object key where the value is an array
+        Map<String, List<RestTestTransformByParentArray>> arrayByObjectKeyFinders = transformations.stream()
+            .filter(transform -> transform instanceof RestTestTransformByParentArray)
+            .map(transform -> (RestTestTransformByParentArray) transform)
+            .collect(Collectors.groupingBy(RestTestTransformByParentArray::getKeyOfArrayToFind));
 
         // transform the tests and include the global setup and teardown as part of the transform
         for (ObjectNode test : tests) {
@@ -62,7 +69,7 @@ public class RestTestTransformer {
                 if ("teardown".equals(testName)) {
                     teardownSection = test;
                 }
-                traverseTest(test, objectKeyFinders);
+                traverseTest(new RestTestContext(testName), test, null, objectKeyFinders, arrayByObjectKeyFinders);
             }
         }
 
@@ -98,19 +105,52 @@ public class RestTestTransformer {
     /**
      * Recursive method to traverse the test.
      *
-     * @param currentNode      The current node that is being evaluated.
-     * @param objectKeyFinders A Map of object keys to find and their associated transformation
+     * @param testContext             A pojo to hold information about the current state of the test that is being traversed.
+     * @param currentNode             The current node that is being evaluated.
+     * @param parentKeyName           The name of the parent key object for the current node. null if none.
+     * @param objectKeyFinders        A Map of object keys to find and their associated transformation by parent Object
+     * @param arrayByObjectKeyFinders A Map of object keys to find and their associated transformation by parent Array
      */
-    private void traverseTest(JsonNode currentNode, Map<String, RestTestTransformByObjectKey> objectKeyFinders) {
+    private void traverseTest(
+        RestTestContext testContext,
+        JsonNode currentNode,
+        String parentKeyName,
+        Map<String, List<RestTestTransformByParentObject>> objectKeyFinders,
+        Map<String, List<RestTestTransformByParentArray>> arrayByObjectKeyFinders
+    ) {
         if (currentNode.isArray()) {
-            currentNode.elements().forEachRemaining(node -> { traverseTest(node, objectKeyFinders); });
+            if (parentKeyName != null) {
+                List<RestTestTransformByParentArray> transforms = arrayByObjectKeyFinders.get(parentKeyName);
+                if (transforms != null) {
+                    for (RestTestTransformByParentArray transform : transforms) {
+                        if (transform.shouldApply(testContext)) {
+                            transform.transformTest((ArrayNode) currentNode);
+                        }
+                    }
+                }
+            }
+            currentNode.elements()
+                .forEachRemaining(node -> { traverseTest(testContext, node, parentKeyName, objectKeyFinders, arrayByObjectKeyFinders); });
         } else if (currentNode.isObject()) {
             currentNode.fields().forEachRemaining(entry -> {
-                RestTestTransformByObjectKey transform = objectKeyFinders.get(entry.getKey());
-                if (transform == null) {
-                    traverseTest(entry.getValue(), objectKeyFinders);
+                List<RestTestTransformByParentObject> transforms = objectKeyFinders.get(entry.getKey());
+                if (transforms == null) {
+                    traverseTest(testContext, entry.getValue(), entry.getKey(), objectKeyFinders, arrayByObjectKeyFinders);
                 } else {
-                    transform.transformTest((ObjectNode) currentNode);
+                    for (RestTestTransformByParentObject transform : transforms) {
+                        if (transform.shouldApply(testContext)) {
+                            if (transform.requiredChildKey() == null) {
+                                transform.transformTest((ObjectNode) currentNode);
+                            } else {
+                                if (entry.getValue().isObject()) {
+                                    ObjectNode child = (ObjectNode) entry.getValue();
+                                    if (child.has(transform.requiredChildKey())) {
+                                        transform.transformTest((ObjectNode) currentNode);
+                                    }
+                                }
+                            }
+                        }
+                    }
                 }
             });
         }

+ 12 - 2
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/InjectHeaders.java → buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/headers/InjectHeaders.java

@@ -6,13 +6,18 @@
  * Side Public License, v 1.
  */
 
-package org.elasticsearch.gradle.test.rest.transform;
+package org.elasticsearch.gradle.test.rest.transform.headers;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransform;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformByParentObject;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformGlobalSetup;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformGlobalTeardown;
+import org.gradle.api.tasks.Input;
 
 import javax.annotation.Nullable;
 import java.util.Iterator;
@@ -22,7 +27,7 @@ import java.util.Map;
  * A {@link RestTestTransform} that injects HTTP headers into a REST test. This includes adding the necessary values to the "do" section
  * as well as adding headers as a features to the "setup" and "teardown" sections.
  */
-public class InjectHeaders implements RestTestTransformByObjectKey, RestTestTransformGlobalSetup, RestTestTransformGlobalTeardown {
+public class InjectHeaders implements RestTestTransformByParentObject, RestTestTransformGlobalSetup, RestTestTransformGlobalTeardown {
 
     private static JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(false);
 
@@ -147,4 +152,9 @@ public class InjectHeaders implements RestTestTransformByObjectKey, RestTestTran
             skipNode.set("skip", featuresNode);
         }
     }
+
+    @Input
+    public Map<String, String> getHeaders() {
+        return headers;
+    }
 }

+ 71 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/AddMatch.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.elasticsearch.gradle.test.rest.transform.RestTestContext;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformByParentArray;
+import org.gradle.api.tasks.Input;
+
+import java.util.Objects;
+
+/**
+ * Adds a match for a REST test. For example add the follow where it did not exist prior to running this: "match":{"_type": "foo"}
+ */
+public class AddMatch implements RestTestTransformByParentArray {
+    private static JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(false);
+    private final String matchKey;
+    private final String testName;
+    private final JsonNode matchValue;
+
+    public AddMatch(String matchKey, JsonNode matchValue, String testName) {
+        this.matchKey = matchKey;
+        this.matchValue = matchValue;
+        this.testName = Objects.requireNonNull(testName, "adding matches is only supported for named tests");
+    }
+
+    @Override
+    public boolean shouldApply(RestTestContext testContext) {
+        return testContext.getTestName().equals(testName);
+    }
+
+    @Override
+    public void transformTest(ArrayNode matchParent) {
+        ObjectNode matchObject = new ObjectNode(jsonNodeFactory);
+        ObjectNode matchContent = new ObjectNode(jsonNodeFactory);
+        matchContent.set(matchKey, matchValue);
+        matchObject.set("match", matchContent);
+        matchParent.add(matchObject);
+    }
+
+    @Override
+    public String getKeyOfArrayToFind() {
+        // match objects are always in the array that is the direct child of the test name, i.e.
+        // "my test name" : [ {"do" : ... }, { "match" : .... }]
+        return testName;
+    }
+
+    @Input
+    public String getMatchKey() {
+        return matchKey;
+    }
+
+    @Input
+    public String getTestName() {
+        return testName;
+    }
+
+    @Input
+    public JsonNode getMatchValue() {
+        return matchValue;
+    }
+}

+ 66 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/RemoveMatch.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.elasticsearch.gradle.test.rest.transform.RestTestContext;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformByParentObject;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.Optional;
+
+/**
+ * A transformation to remove the key/value of a given match. To help keep logic simple, an empty match object will be left behind
+ * if/when the only key/value is removed.
+ */
+public class RemoveMatch implements RestTestTransformByParentObject {
+    private final String removeKey;
+    private final String testName;
+
+    public RemoveMatch(String removeKey) {
+        this.removeKey = removeKey;
+        this.testName = null;
+    }
+
+    public RemoveMatch(String removeKey, String testName) {
+        this.removeKey = removeKey;
+        this.testName = testName;
+    }
+
+    @Override
+    public String getKeyToFind() {
+        return "match";
+    }
+
+    @Override
+    public String requiredChildKey() {
+        return removeKey;
+    }
+
+    @Override
+    public boolean shouldApply(RestTestContext testContext) {
+        return testName == null || testContext.getTestName().equals(testName);
+    }
+
+    @Override
+    public void transformTest(ObjectNode matchParent) {
+        ObjectNode matchObject = (ObjectNode) matchParent.get(getKeyToFind());
+        matchObject.remove(removeKey);
+    }
+
+    @Input
+    public String getRemoveKey() {
+        return removeKey;
+    }
+
+    @Input
+    @Optional
+    public String getTestName() {
+        return testName;
+    }
+}

+ 75 - 0
buildSrc/src/main/java/org/elasticsearch/gradle/test/rest/transform/match/ReplaceMatch.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.elasticsearch.gradle.test.rest.transform.RestTestContext;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformByParentObject;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.Optional;
+
+/**
+ * A transformation to replace the value of a match. For example, change from "match":{"_type": "foo"} to "match":{"_type": "bar"}
+ */
+public class ReplaceMatch implements RestTestTransformByParentObject {
+    private final String replaceKey;
+    private final JsonNode replacementNode;
+    private final String testName;
+
+    public ReplaceMatch(String replaceKey, JsonNode replacementNode) {
+
+        this.replaceKey = replaceKey;
+        this.replacementNode = replacementNode;
+        this.testName = null;
+    }
+
+    public ReplaceMatch(String replaceKey, JsonNode replacementNode, String testName) {
+        this.replaceKey = replaceKey;
+        this.replacementNode = replacementNode;
+        this.testName = testName;
+    }
+
+    @Override
+    public String getKeyToFind() {
+        return "match";
+    }
+
+    @Override
+    public String requiredChildKey() {
+        return replaceKey;
+    }
+
+    @Override
+    public boolean shouldApply(RestTestContext testContext) {
+        return testName == null || testContext.getTestName().equals(testName);
+    }
+
+    @Override
+    public void transformTest(ObjectNode matchParent) {
+        ObjectNode matchNode = (ObjectNode) matchParent.get(getKeyToFind());
+        matchNode.set(replaceKey, replacementNode);
+    }
+
+    @Input
+    public String getReplaceKey() {
+        return replaceKey;
+    }
+
+    @Input
+    public JsonNode getReplacementNode() {
+        return replacementNode;
+    }
+
+    @Input
+    @Optional
+    public String getTestName() {
+        return testName;
+    }
+}

+ 8 - 6
buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/InjectHeaderTests.java → buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/header/InjectHeaderTests.java

@@ -6,7 +6,7 @@
  * Side Public License, v 1.
  */
 
-package org.elasticsearch.gradle.test.rest.transform;
+package org.elasticsearch.gradle.test.rest.transform.header;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.node.TextNode;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
 import org.elasticsearch.gradle.test.GradleUnitTestCase;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformer;
+import org.elasticsearch.gradle.test.rest.transform.headers.InjectHeaders;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.core.IsCollectionContaining;
 import org.junit.Test;
@@ -52,7 +54,7 @@ public class InjectHeaderTests extends GradleUnitTestCase {
      */
     @Test
     public void testInjectHeadersWithoutSetupBlock() throws Exception {
-        String testName = "/rest/header_inject/no_setup.yml";
+        String testName = "/rest/transform/header/no_setup.yml";
         File testFile = new File(getClass().getResource(testName).toURI());
         YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
         List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
@@ -86,7 +88,7 @@ public class InjectHeaderTests extends GradleUnitTestCase {
      */
     @Test
     public void testInjectHeadersWithSetupBlock() throws Exception {
-        String testName = "/rest/header_inject/with_setup.yml";
+        String testName = "/rest/transform/header/with_setup.yml";
         File testFile = new File(getClass().getResource(testName).toURI());
         YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
         List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
@@ -121,7 +123,7 @@ public class InjectHeaderTests extends GradleUnitTestCase {
      */
     @Test
     public void testInjectHeadersWithSkipBlock() throws Exception {
-        String testName = "/rest/header_inject/with_skip.yml";
+        String testName = "/rest/transform/header/with_skip.yml";
         File testFile = new File(getClass().getResource(testName).toURI());
         YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
         List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
@@ -168,7 +170,7 @@ public class InjectHeaderTests extends GradleUnitTestCase {
      */
     @Test
     public void testInjectHeadersWithFeaturesBlock() throws Exception {
-        String testName = "/rest/header_inject/with_features.yml";
+        String testName = "/rest/transform/header/with_features.yml";
         File testFile = new File(getClass().getResource(testName).toURI());
         YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
         List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
@@ -214,7 +216,7 @@ public class InjectHeaderTests extends GradleUnitTestCase {
      */
     @Test
     public void testInjectHeadersWithHeadersBlock() throws Exception {
-        String testName = "/rest/header_inject/with_headers.yml";
+        String testName = "/rest/transform/header/with_headers.yml";
         File testFile = new File(getClass().getResource(testName).toURI());
         YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
         List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();

+ 175 - 0
buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/AddMatchTests.java

@@ -0,0 +1,175 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
+import org.elasticsearch.gradle.test.GradleUnitTestCase;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformer;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AddMatchTests extends GradleUnitTestCase {
+
+    private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
+    private static final ObjectMapper MAPPER = new ObjectMapper(YAML_FACTORY);
+    private static final ObjectReader READER = MAPPER.readerFor(ObjectNode.class);
+
+    private static final boolean humanDebug = false; // useful for humans trying to debug these tests
+
+    @Test
+    public void testAddAllNotSupported() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        JsonNode addNode = MAPPER.convertValue("_doc", JsonNode.class);
+        assertEquals(
+            "adding matches is only supported for named tests",
+            expectThrows(
+                NullPointerException.class,
+                () -> transformer.transformRestTests(
+                    new LinkedList<>(tests),
+                    Collections.singletonList(new AddMatch("_type", addNode, null))
+                )
+            ).getMessage()
+        );
+
+    }
+
+    @Test
+    public void testAddByTest() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        JsonNode addNode = MAPPER.convertValue(123456789, JsonNode.class);
+        validateTest(tests, true);
+        List<ObjectNode> transformedTests = transformer.transformRestTests(
+            new LinkedList<>(tests),
+            Collections.singletonList(new AddMatch("my_number", addNode, "Basic"))
+        );
+        printTest(testName, transformedTests);
+        validateTest(tests, false);
+    }
+
+    private void validateTest(List<ObjectNode> tests, boolean beforeTransformation) {
+        ObjectNode setUp = tests.get(0);
+        assertThat(setUp.get("setup"), CoreMatchers.notNullValue());
+        ObjectNode tearDown = tests.get(1);
+        assertThat(tearDown.get("teardown"), CoreMatchers.notNullValue());
+        ObjectNode firstTest = tests.get(2);
+        assertThat(firstTest.get("Test that queries on _index match against the correct indices."), CoreMatchers.notNullValue());
+        ObjectNode lastTest = tests.get(tests.size() - 1);
+        assertThat(lastTest.get("Basic"), CoreMatchers.notNullValue());
+
+        // setup
+        JsonNode setup = setUp.get("setup");
+        assertThat(setup, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode setupParentArray = (ArrayNode) setup;
+
+        AtomicBoolean setUpHasMatchObject = new AtomicBoolean(false);
+        setupParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                setUpHasMatchObject.set(true);
+            }
+        });
+        assertFalse(setUpHasMatchObject.get());
+
+        // teardown
+        JsonNode teardown = tearDown.get("teardown");
+        assertThat(teardown, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode teardownParentArray = (ArrayNode) teardown;
+
+        AtomicBoolean teardownHasMatchObject = new AtomicBoolean(false);
+        teardownParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                teardownHasMatchObject.set(true);
+            }
+        });
+        assertFalse(teardownHasMatchObject.get());
+
+        // first test
+        JsonNode firstTestChild = firstTest.get("Test that queries on _index match against the correct indices.");
+        assertThat(firstTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode firstTestParentArray = (ArrayNode) firstTestChild;
+
+        AtomicBoolean firstTestHasMatchObject = new AtomicBoolean(false);
+        firstTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                firstTestHasMatchObject.set(true);
+            }
+        });
+        assertTrue(firstTestHasMatchObject.get());
+
+        // last test
+        JsonNode lastTestChild = lastTest.get("Basic");
+        assertThat(lastTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode lastTestParentArray = (ArrayNode) lastTestChild;
+
+        AtomicBoolean lastTestHasMatchObject = new AtomicBoolean(false);
+        AtomicBoolean lastTestHasAddedObject = new AtomicBoolean(false);
+        lastTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                lastTestHasMatchObject.set(true);
+                if (lastTestHasAddedObject.get() == false && matchObject.get("my_number") != null) {
+                    lastTestHasAddedObject.set(true);
+                }
+
+            }
+        });
+        assertTrue(lastTestHasMatchObject.get());
+        if (beforeTransformation) {
+            assertFalse(lastTestHasAddedObject.get());
+        } else {
+            assertTrue(lastTestHasAddedObject.get());
+        }
+    }
+
+    // only to help manually debug
+    private void printTest(String testName, List<ObjectNode> tests) {
+        if (humanDebug) {
+            System.out.println("\n************* " + testName + " *************");
+            try (SequenceWriter sequenceWriter = MAPPER.writer().writeValues(System.out)) {
+                for (ObjectNode transformedTest : tests) {
+                    sequenceWriter.write(transformedTest);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 209 - 0
buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/RemoveMatchTests.java

@@ -0,0 +1,209 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
+import org.elasticsearch.gradle.test.GradleUnitTestCase;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformer;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class RemoveMatchTests extends GradleUnitTestCase {
+
+    private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
+    private static final ObjectMapper MAPPER = new ObjectMapper(YAML_FACTORY);
+    private static final ObjectReader READER = MAPPER.readerFor(ObjectNode.class);
+
+    private static final boolean humanDebug = false; // useful for humans trying to debug these tests
+
+    @Test
+    public void testRemoveAll() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        validateTest(tests, true, true);
+        List<ObjectNode> transformedTests = transformer.transformRestTests(
+            new LinkedList<>(tests),
+            Collections.singletonList(new RemoveMatch("_type"))
+        );
+        printTest(testName, transformedTests);
+        validateTest(tests, false, true);
+    }
+
+    @Test
+    public void testRemoveByTest() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        validateTest(tests, true, false);
+        List<ObjectNode> transformedTests = transformer.transformRestTests(
+            new LinkedList<>(tests),
+            Collections.singletonList(new RemoveMatch("_type", "Basic"))
+        );
+        printTest(testName, transformedTests);
+        validateTest(tests, false, false);
+
+    }
+
+    private void validateTest(List<ObjectNode> tests, boolean beforeTransformation, boolean allTests) {
+        ObjectNode setUp = tests.get(0);
+        assertThat(setUp.get("setup"), CoreMatchers.notNullValue());
+        ObjectNode tearDown = tests.get(1);
+        assertThat(tearDown.get("teardown"), CoreMatchers.notNullValue());
+        ObjectNode firstTest = tests.get(2);
+        assertThat(firstTest.get("Test that queries on _index match against the correct indices."), CoreMatchers.notNullValue());
+        ObjectNode lastTest = tests.get(tests.size() - 1);
+        assertThat(lastTest.get("Basic"), CoreMatchers.notNullValue());
+
+        // setup
+        JsonNode setup = setUp.get("setup");
+        assertThat(setup, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode setupParentArray = (ArrayNode) setup;
+
+        AtomicBoolean setUpHasMatchObject = new AtomicBoolean(false);
+        setupParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                setUpHasMatchObject.set(true);
+            }
+        });
+        assertFalse(setUpHasMatchObject.get());
+
+        // teardown
+        JsonNode teardown = tearDown.get("teardown");
+        assertThat(teardown, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode teardownParentArray = (ArrayNode) teardown;
+
+        AtomicBoolean teardownHasMatchObject = new AtomicBoolean(false);
+        teardownParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                teardownHasMatchObject.set(true);
+            }
+        });
+        assertFalse(teardownHasMatchObject.get());
+
+        // first test
+        JsonNode firstTestChild = firstTest.get("Test that queries on _index match against the correct indices.");
+        assertThat(firstTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode firstTestParentArray = (ArrayNode) firstTestChild;
+
+        AtomicBoolean firstTestHasMatchObject = new AtomicBoolean(false);
+        AtomicBoolean firstTestHasTypeMatch = new AtomicBoolean(false);
+
+        firstTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                firstTestHasMatchObject.set(true);
+                if (firstTestHasTypeMatch.get() == false && matchObject.get("_type") != null) {
+                    firstTestHasTypeMatch.set(true);
+                }
+            }
+        });
+        assertTrue(firstTestHasMatchObject.get());
+        if (beforeTransformation) {
+            assertTrue(firstTestHasTypeMatch.get());
+        } else {
+            if (allTests) {
+                assertFalse(firstTestHasTypeMatch.get());
+            } else {
+                assertTrue(firstTestHasTypeMatch.get());
+            }
+        }
+
+        // last test
+        JsonNode lastTestChild = lastTest.get("Basic");
+        assertThat(lastTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode lastTestParentArray = (ArrayNode) lastTestChild;
+
+        AtomicBoolean lastTestHasMatchObject = new AtomicBoolean(false);
+        AtomicBoolean lastTestHasTypeMatch = new AtomicBoolean(false);
+        lastTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                lastTestHasMatchObject.set(true);
+                if (lastTestHasTypeMatch.get() == false && matchObject.get("_type") != null) {
+                    lastTestHasTypeMatch.set(true);
+                }
+            }
+        });
+        assertTrue(lastTestHasMatchObject.get());
+        if (beforeTransformation) {
+            assertTrue(lastTestHasTypeMatch.get());
+        } else {
+            assertFalse(lastTestHasTypeMatch.get());
+        }
+
+        // exclude setup, teardown, first test, and last test
+        for (int i = 3; i <= tests.size() - 2; i++) {
+            ObjectNode otherTest = tests.get(i);
+
+            JsonNode otherTestChild = otherTest.get(otherTest.fields().next().getKey());
+            assertThat(otherTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+            ArrayNode otherTestParentArray = (ArrayNode) otherTestChild;
+            AtomicBoolean otherTestHasTypeMatch = new AtomicBoolean(false);
+            otherTestParentArray.elements().forEachRemaining(node -> {
+                assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+                ObjectNode childObject = (ObjectNode) node;
+                JsonNode matchObject = childObject.get("match");
+                if (matchObject != null) {
+                    if (beforeTransformation == false) {
+                        if (otherTestHasTypeMatch.get() == false && matchObject.get("_type") != null) {
+                            otherTestHasTypeMatch.set(true);
+                        }
+                    }
+                }
+
+                if (allTests) {
+                    assertFalse(otherTestHasTypeMatch.get());
+                }
+            });
+        }
+    }
+
+    // only to help manually debug
+    private void printTest(String testName, List<ObjectNode> tests) {
+        if (humanDebug) {
+            System.out.println("\n************* " + testName + " *************");
+            try (SequenceWriter sequenceWriter = MAPPER.writer().writeValues(System.out)) {
+                for (ObjectNode transformedTest : tests) {
+                    sequenceWriter.write(transformedTest);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 200 - 0
buildSrc/src/test/java/org/elasticsearch/gradle/test/rest/transform/match/ReplaceMatchTests.java

@@ -0,0 +1,200 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.gradle.test.rest.transform.match;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
+import org.elasticsearch.gradle.test.GradleUnitTestCase;
+import org.elasticsearch.gradle.test.rest.transform.RestTestTransformer;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ReplaceMatchTests extends GradleUnitTestCase {
+
+    private static final YAMLFactory YAML_FACTORY = new YAMLFactory();
+    private static final ObjectMapper MAPPER = new ObjectMapper(YAML_FACTORY);
+    private static final ObjectReader READER = MAPPER.readerFor(ObjectNode.class);
+
+    private static final boolean humanDebug = false; // useful for humans trying to debug these tests
+
+    @Test
+    public void testReplaceAll() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        JsonNode replacementNode = MAPPER.convertValue("_replaced_type", JsonNode.class);
+        validateTest(tests, true, true);
+        List<ObjectNode> transformedTests = transformer.transformRestTests(
+            new LinkedList<>(tests),
+            Collections.singletonList(new ReplaceMatch("_type", replacementNode, null))
+        );
+        printTest(testName, transformedTests);
+        validateTest(tests, false, true);
+
+    }
+
+    @Test
+    public void testReplaceByTest() throws Exception {
+        String testName = "/rest/transform/match/match.yml";
+        File testFile = new File(getClass().getResource(testName).toURI());
+        YAMLParser yamlParser = YAML_FACTORY.createParser(testFile);
+        List<ObjectNode> tests = READER.<ObjectNode>readValues(yamlParser).readAll();
+        RestTestTransformer transformer = new RestTestTransformer();
+        JsonNode replacementNode = MAPPER.convertValue("_replaced_type", JsonNode.class);
+        validateTest(tests, true, false);
+        List<ObjectNode> transformedTests = transformer.transformRestTests(
+            new LinkedList<>(tests),
+            Collections.singletonList(new ReplaceMatch("_type", replacementNode, "Basic"))
+        );
+        printTest(testName, transformedTests);
+        validateTest(tests, false, false);
+    }
+
+    private void validateTest(List<ObjectNode> tests, boolean beforeTransformation, boolean allTests) {
+        ObjectNode setUp = tests.get(0);
+        assertThat(setUp.get("setup"), CoreMatchers.notNullValue());
+        ObjectNode tearDown = tests.get(1);
+        assertThat(tearDown.get("teardown"), CoreMatchers.notNullValue());
+        ObjectNode firstTest = tests.get(2);
+        assertThat(firstTest.get("Test that queries on _index match against the correct indices."), CoreMatchers.notNullValue());
+        ObjectNode lastTest = tests.get(tests.size() - 1);
+        assertThat(lastTest.get("Basic"), CoreMatchers.notNullValue());
+
+        // setup
+        JsonNode setup = setUp.get("setup");
+        assertThat(setup, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode setupParentArray = (ArrayNode) setup;
+
+        AtomicBoolean setUpHasMatchObject = new AtomicBoolean(false);
+        setupParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                setUpHasMatchObject.set(true);
+            }
+        });
+        assertFalse(setUpHasMatchObject.get());
+
+        // teardown
+        JsonNode teardown = tearDown.get("teardown");
+        assertThat(teardown, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode teardownParentArray = (ArrayNode) teardown;
+
+        AtomicBoolean teardownHasMatchObject = new AtomicBoolean(false);
+        teardownParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                teardownHasMatchObject.set(true);
+            }
+        });
+        assertFalse(teardownHasMatchObject.get());
+
+        // first test
+        JsonNode firstTestChild = firstTest.get("Test that queries on _index match against the correct indices.");
+        assertThat(firstTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode firstTestParentArray = (ArrayNode) firstTestChild;
+
+        AtomicBoolean firstTestHasMatchObject = new AtomicBoolean(false);
+        AtomicBoolean firstTestHasTypeMatch = new AtomicBoolean(false);
+
+        firstTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                firstTestHasMatchObject.set(true);
+                if (firstTestHasTypeMatch.get() == false && matchObject.get("_type") != null) {
+                    firstTestHasTypeMatch.set(true);
+                }
+                if (matchObject.get("_type") != null && beforeTransformation == false && allTests) {
+                    assertThat(matchObject.get("_type").asText(), CoreMatchers.is("_replaced_type"));
+                }
+            }
+        });
+        assertTrue(firstTestHasMatchObject.get());
+        assertTrue(firstTestHasTypeMatch.get());
+
+        // last test
+        JsonNode lastTestChild = lastTest.get("Basic");
+        assertThat(lastTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+        ArrayNode lastTestParentArray = (ArrayNode) lastTestChild;
+
+        AtomicBoolean lastTestHasMatchObject = new AtomicBoolean(false);
+        AtomicBoolean lastTestHasTypeMatch = new AtomicBoolean(false);
+        lastTestParentArray.elements().forEachRemaining(node -> {
+            assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+            ObjectNode childObject = (ObjectNode) node;
+            JsonNode matchObject = childObject.get("match");
+            if (matchObject != null) {
+                lastTestHasMatchObject.set(true);
+                if (lastTestHasTypeMatch.get() == false && matchObject.get("_type") != null) {
+                    lastTestHasTypeMatch.set(true);
+                }
+                if (matchObject.get("_type") != null && beforeTransformation == false) {
+                    assertThat(matchObject.get("_type").asText(), CoreMatchers.is("_replaced_type"));
+                }
+            }
+        });
+        assertTrue(lastTestHasMatchObject.get());
+        assertTrue(lastTestHasTypeMatch.get());
+
+        // exclude setup, teardown, first test, and last test
+        for (int i = 3; i <= tests.size() - 2; i++) {
+            ObjectNode otherTest = tests.get(i);
+            JsonNode otherTestChild = otherTest.get(otherTest.fields().next().getKey());
+            assertThat(otherTestChild, CoreMatchers.instanceOf(ArrayNode.class));
+            ArrayNode otherTestParentArray = (ArrayNode) otherTestChild;
+            AtomicBoolean otherTestHasTypeMatch = new AtomicBoolean(false);
+            otherTestParentArray.elements().forEachRemaining(node -> {
+                assertThat(node, CoreMatchers.instanceOf(ObjectNode.class));
+                ObjectNode childObject = (ObjectNode) node;
+                JsonNode matchObject = childObject.get("match");
+                if (matchObject != null) {
+                    if (matchObject.get("_type") != null) {
+                        if (matchObject.get("_type") != null && beforeTransformation == false && allTests) {
+                            assertThat(matchObject.get("_type").asText(), CoreMatchers.is("_replaced_type"));
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    // only to help manually debug
+    private void printTest(String testName, List<ObjectNode> tests) {
+        if (humanDebug) {
+            System.out.println("\n************* " + testName + " *************");
+            try (SequenceWriter sequenceWriter = MAPPER.writer().writeValues(System.out)) {
+                for (ObjectNode transformedTest : tests) {
+                    sequenceWriter.write(transformedTest);
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

+ 0 - 0
buildSrc/src/test/resources/rest/header_inject/no_setup.yml → buildSrc/src/test/resources/rest/transform/header/no_setup.yml


+ 0 - 0
buildSrc/src/test/resources/rest/header_inject/with_features.yml → buildSrc/src/test/resources/rest/transform/header/with_features.yml


+ 0 - 0
buildSrc/src/test/resources/rest/header_inject/with_headers.yml → buildSrc/src/test/resources/rest/transform/header/with_headers.yml


+ 0 - 0
buildSrc/src/test/resources/rest/header_inject/with_setup.yml → buildSrc/src/test/resources/rest/transform/header/with_setup.yml


+ 0 - 0
buildSrc/src/test/resources/rest/header_inject/with_skip.yml → buildSrc/src/test/resources/rest/transform/header/with_skip.yml


+ 181 - 0
buildSrc/src/test/resources/rest/transform/match/match.yml

@@ -0,0 +1,181 @@
+---
+setup:
+  - do:
+      indices.create:
+        index: single_doc_index
+        body:
+          settings:
+            index:
+              number_of_shards: 1
+              number_of_replicas: 0
+---
+teardown:
+  - do:
+      indices.delete:
+        index: single_doc_index
+        ignore_unavailable: true
+
+---
+"Test that queries on _index match against the correct indices.":
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "single_doc_index"}}'
+          - '{"f1": "local_cluster", "sort_field": 0}'
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        body:
+          query:
+            term:
+              "_index": "single_doc_index"
+
+  - match: { hits.total: 1 }
+  - match: { hits.hits.0._index: "single_doc_index"}
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 0}
+  - match: { _shards.failed: 0 }
+
+  - do:
+      search:
+        rest_total_hits_as_int: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        body:
+          query:
+            term:
+              "_index": "my_remote_cluster:single_doc_index"
+
+  - match: { hits.total: 1 }
+  - match: { hits.hits.0._index: "my_remote_cluster:single_doc_index"}
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 0}
+  - match: { _shards.failed: 0 }
+  - match: { _type:    foo   }
+
+---
+"Test that queries on _index that don't match are skipped":
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "single_doc_index"}}'
+          - '{"f1": "local_cluster", "sort_field": 0}'
+
+  - do:
+      search:
+        ccs_minimize_roundtrips: false
+        track_total_hits: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        pre_filter_shard_size: 1
+        body:
+          query:
+            term:
+              "_index": "does_not_match"
+
+  - match: { hits.total.value: 0 }
+  - match: { _type:    test   }
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 1}
+  - match: { _shards.failed: 0 }
+
+  - do:
+      search:
+        ccs_minimize_roundtrips: false
+        track_total_hits: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        pre_filter_shard_size: 1
+        body:
+          query:
+            term:
+              "_index": "my_remote_cluster:does_not_match"
+
+  - match: { hits.total.value: 0 }
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 1}
+  - match: { _shards.failed: 0 }
+
+---
+"no _type":
+
+  - do:
+      bulk:
+        refresh: true
+        body:
+          - '{"index": {"_index": "single_doc_index"}}'
+          - '{"f1": "local_cluster", "sort_field": 0}'
+
+  - do:
+      search:
+        ccs_minimize_roundtrips: false
+        track_total_hits: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        pre_filter_shard_size: 1
+        body:
+          query:
+            term:
+              "_index": "does_not_match"
+
+  - match: { hits.total.value: 0 }
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 1}
+  - match: { _shards.failed: 0 }
+
+  - do:
+      search:
+        ccs_minimize_roundtrips: false
+        track_total_hits: true
+        index: "single_doc_index,my_remote_cluster:single_doc_index"
+        pre_filter_shard_size: 1
+        body:
+          query:
+            term:
+              "_index": "my_remote_cluster:does_not_match"
+
+  - match: { hits.total.value: 0 }
+  - match: { _shards.total: 2 }
+  - match: { _shards.successful: 2 }
+  - match: { _shards.skipped : 1}
+  - match: { _shards.failed: 0 }
+---
+"Basic":
+
+  - do:
+      index:
+        index: test_1
+        type:  test
+        id:    中文
+        body:  { "foo": "Hello: 中文" }
+
+  - do:
+      get:
+        index: test_1
+        type:  test
+        id:    中文
+
+  - match: { _index:   test_1 }
+  - match: { _type:    test   }
+  - match: { _id:      中文      }
+  - match: { _source:  { foo: "Hello: 中文" } }
+
+  - do:
+      get:
+        index: test_1
+        type: _all
+        id:    中文
+
+  - match: { _index:   test_1 }
+  - match: { _type:    test   }
+  - match: { _id:      中文      }
+  - match: { _source:  { foo: "Hello: 中文" } }
+
+