diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..6072fee3e1af --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rest-spec"] + path = rest-spec + url = git@github.com:elasticsearch/elasticsearch-rest-api-spec.git diff --git a/TESTING.asciidoc b/TESTING.asciidoc index 4079b99c43c0..d07a8c7506f7 100644 --- a/TESTING.asciidoc +++ b/TESTING.asciidoc @@ -166,3 +166,45 @@ even if tests are passing. ------------------------------ mvn test -Dtests.output=always ------------------------------ + +== Testing the REST layer + +The available integration tests make use of the java API to communicate with +the elasticsearch nodes, using the internal binary transport (port 9300 by +default). +The REST layer is tested through specific tests that are shared between all +the elasticsearch official clients and can be found on the +https://github.com/elasticsearch/elasticsearch-rest-api-spec[elasticsearch-rest-api-spec project]. +They consist of +https://github.com/elasticsearch/elasticsearch-rest-api-spec/tree/master/test[YAML files] +that describe the operations to be executed and the obtained results that +need to be tested. + +`ElasticsearchRestTests` is the executable test class that runs all the +yaml suites available through a git submodule within the `rest-spec` folder. +The submodule gets automatically initialized through maven right before +running tests (generate-test-resources phase). +The REST tests cannot be run without the files pulled from the submodule, +thus if the `rest-spec` folder is empty on your working copy, it means +that it needs to be initialized with the following command: + +------------------------------ +git submodule update --init +------------------------------ + +The following are the options supported by the REST tests runner: + +* `tests.rest[true|false|host:port]`: determines whether the REST tests need +to be run and if so whether to rely on an external cluster (providing host +and port) or fire a test cluster (default) +* `tests.rest.suite`: comma separated paths of the test suites to be run +(by default loaded from /rest-spec/test). It is possible to run only a subset +of the tests providing a sub-folder or even a single yaml file (the default +/rest-spec/test prefix is optional when files are loaded from classpath) +e.g. -Dtests.rest.suite=index,get,create/10_with_id +* `tests.rest.spec`: REST spec path (default /rest-spec/api) +* `tests.iters`: runs multiple iterations +* `tests.seed`: seed to base the random behaviours on +* `tests.appendseed[true|false]`: enables adding the seed to each test +section's description (default false) +* `tests.cluster_seed`: seed used to create the test cluster (if enabled) \ No newline at end of file diff --git a/pom.xml b/pom.xml index e70a6c89b3c0..82063e9111c3 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,8 @@ true onerror + false + false INFO @@ -65,6 +67,12 @@ ${lucene.version} test + + org.apache.httpcomponents + httpclient + 4.3.1 + test + org.apache.lucene @@ -313,6 +321,14 @@ **/*.* + + ${basedir}/rest-spec + rest-spec + + api/*.json + test/**/*.yaml + + @@ -396,6 +412,7 @@ ${tests.jvm.argline} + ${tests.appendseed} ${tests.iters} ${tests.maxfailures} ${tests.failfast} @@ -412,6 +429,9 @@ ${tests.integration} ${tests.cluster_seed} ${tests.client.ratio} + ${tests.rest} + ${tests.rest.suite} + ${tests.rest.spec} ${env.ES_TEST_LOCAL} ${es.node.mode} ${es.logger.level} @@ -1002,14 +1022,50 @@ exec + + java + + -version + + + + + Init Rest Spec + generate-test-resources + + exec + + + ${rest.init.skip} + git + + submodule + update + --init + + + + + Pull Rest Spec + generate-test-resources + + exec + + + ${rest.pull.skip} + git + + submodule + foreach + git + pull + origin + master + + - - java - - -version - - + org.apache.maven.plugins @@ -1026,7 +1082,14 @@ org/elasticsearch/test/**/* org/apache/lucene/util/AbstractRandomizedTest.class org/apache/lucene/util/AbstractRandomizedTest$*.class + com/carrotsearch/randomizedtesting/StandaloneRandomizedContext.class + + + org/elasticsearch/test/rest/ElasticsearchRestTests.class + + org/elasticsearch/test/rest/test/**/* + diff --git a/rest-spec b/rest-spec new file mode 160000 index 000000000000..2f5f78f24d8f --- /dev/null +++ b/rest-spec @@ -0,0 +1 @@ +Subproject commit 2f5f78f24d8fbacf69c83ab7545654c83965e846 diff --git a/src/test/java/com/carrotsearch/randomizedtesting/StandaloneRandomizedContext.java b/src/test/java/com/carrotsearch/randomizedtesting/StandaloneRandomizedContext.java new file mode 100644 index 000000000000..8816f6e553eb --- /dev/null +++ b/src/test/java/com/carrotsearch/randomizedtesting/StandaloneRandomizedContext.java @@ -0,0 +1,68 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.carrotsearch.randomizedtesting; + +/** + * Exposes methods that allow to use a {@link RandomizedContext} without using a {@link RandomizedRunner} + * This was specifically needed by the REST tests since they run with a custom junit runner ({@link org.elasticsearch.test.rest.junit.RestTestSuiteRunner}) + */ +public final class StandaloneRandomizedContext { + + private StandaloneRandomizedContext() { + + } + + /** + * Creates a new {@link RandomizedContext} associated to the current thread + */ + public static void createRandomizedContext(Class testClass, Randomness runnerRandomness) { + //the randomized runner is passed in as null, which is fine as long as we don't try to access it afterwards + RandomizedContext randomizedContext = RandomizedContext.create(Thread.currentThread().getThreadGroup(), testClass, null); + randomizedContext.push(runnerRandomness.clone(Thread.currentThread())); + } + + /** + * Destroys the {@link RandomizedContext} associated to the current thread + */ + public static void disposeRandomizedContext() { + RandomizedContext.current().dispose(); + } + + public static void pushRandomness(Randomness randomness) { + RandomizedContext.current().push(randomness); + } + + public static void popAndDestroy() { + RandomizedContext.current().popAndDestroy(); + } + + /** + * Returns the string formatted seed associated to the current thread's randomized context + */ + public static String getSeedAsString() { + return SeedUtils.formatSeed(RandomizedContext.current().getRandomness().getSeed()); + } + + /** + * Util method to extract the seed out of a {@link Randomness} instance + */ + public static long getSeed(Randomness randomness) { + return randomness.getSeed(); + } +} diff --git a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java index 40ea83f459a8..7e573404a076 100644 --- a/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ElasticsearchIntegrationTest.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.test; -import com.carrotsearch.randomizedtesting.SeedUtils; import com.google.common.base.Joiner; import org.apache.lucene.util.AbstractRandomizedTest; import org.elasticsearch.ExceptionsHelper; @@ -68,6 +67,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import static org.elasticsearch.test.TestCluster.SHARED_CLUSTER_SEED; +import static org.elasticsearch.test.TestCluster.clusterName; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.hamcrest.Matchers.emptyIterable; @@ -123,7 +124,7 @@ import static org.hamcrest.Matchers.equalTo; * This class supports the following system properties (passed with -Dkey=value to the application) *
    *
  • -D{@value #TESTS_CLIENT_RATIO} - a double value in the interval [0..1] which defines the ration between node and transport clients used
  • - *
  • -D{@value #TESTS_CLUSTER_SEED} - a random seed used to initialize the clusters random context. + *
  • -D{@value TestCluster#TESTS_CLUSTER_SEED} - a random seed used to initialize the clusters random context. *
  • -D{@value #INDEX_SEED_SETTING} - a random seed used to initialize the index random context. *
*

@@ -132,24 +133,13 @@ import static org.hamcrest.Matchers.equalTo; @AbstractRandomizedTest.IntegrationTests public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase { - - /** - * The random seed for the shared test cluster used in the current JVM. - */ - public static final long SHARED_CLUSTER_SEED = clusterSeed(); - - private static final TestCluster GLOBAL_CLUSTER = new TestCluster(SHARED_CLUSTER_SEED, TestCluster.clusterName("shared", ElasticsearchTestCase.CHILD_VM_ID, SHARED_CLUSTER_SEED)); + private static final TestCluster GLOBAL_CLUSTER = new TestCluster(SHARED_CLUSTER_SEED, clusterName("shared", ElasticsearchTestCase.CHILD_VM_ID, SHARED_CLUSTER_SEED)); /** * Key used to set the transport client ratio via the commandline -D{@value #TESTS_CLIENT_RATIO} */ public static final String TESTS_CLIENT_RATIO = "tests.client.ratio"; - /** - * Key used to set the shared cluster random seed via the commandline -D{@value #TESTS_CLUSTER_SEED} - */ - public static final String TESTS_CLUSTER_SEED = "tests.cluster_seed"; - /** * Key used to retrieve the index random seed from the index settings on a running node. * The value of this seed can be used to initialize a random context for a specific index. @@ -830,7 +820,7 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase }; } - return new TestCluster(currentClusterSeed, numNodes, TestCluster.clusterName(scope.name(), ElasticsearchTestCase.CHILD_VM_ID, currentClusterSeed), nodeSettingsSource); + return new TestCluster(currentClusterSeed, numNodes, clusterName(scope.name(), ElasticsearchTestCase.CHILD_VM_ID, currentClusterSeed), nodeSettingsSource); } /** @@ -859,14 +849,6 @@ public abstract class ElasticsearchIntegrationTest extends ElasticsearchTestCase double transportClientRatio() default -1; } - private static long clusterSeed() { - String property = System.getProperty(TESTS_CLUSTER_SEED); - if (property == null || property.isEmpty()) { - return System.nanoTime(); - } - return SeedUtils.parseSeed(property); - } - /** * Returns the client ratio configured via */ diff --git a/src/test/java/org/elasticsearch/test/TestCluster.java b/src/test/java/org/elasticsearch/test/TestCluster.java index 4803da083697..3fbefd816d5f 100644 --- a/src/test/java/org/elasticsearch/test/TestCluster.java +++ b/src/test/java/org/elasticsearch/test/TestCluster.java @@ -33,6 +33,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; @@ -78,6 +79,24 @@ public final class TestCluster implements Iterable { private final ESLogger logger = Loggers.getLogger(getClass()); + /** + * The random seed for the shared test cluster used in the current JVM. + */ + public static final long SHARED_CLUSTER_SEED = clusterSeed(); + + /** + * Key used to set the shared cluster random seed via the commandline -D{@value #TESTS_CLUSTER_SEED} + */ + public static final String TESTS_CLUSTER_SEED = "tests.cluster_seed"; + + private static long clusterSeed() { + String property = System.getProperty(TESTS_CLUSTER_SEED); + if (!Strings.hasLength(property)) { + return System.nanoTime(); + } + return SeedUtils.parseSeed(property); + } + /* sorted map to make traverse order reproducible */ private final TreeMap nodes = newTreeMap(); @@ -109,6 +128,10 @@ public final class TestCluster implements Iterable { this(clusterSeed, -1, clusterName, NodeSettingsSource.EMPTY); } + public TestCluster(long clusterSeed, int numNodes, String clusterName) { + this(clusterSeed, numNodes, clusterName, NodeSettingsSource.EMPTY); + } + TestCluster(long clusterSeed, int numNodes, String clusterName, NodeSettingsSource nodeSettingsSource) { this.clusterName = clusterName; Random random = new Random(clusterSeed); @@ -164,7 +187,7 @@ public final class TestCluster implements Iterable { return builder.build(); } - static String clusterName(String prefix, String childVMId, long clusterSeed) { + public static String clusterName(String prefix, String childVMId, long clusterSeed) { StringBuilder builder = new StringBuilder(prefix); builder.append('-').append(NetworkUtils.getLocalAddress().getHostName()); builder.append("-CHILD_VM=[").append(childVMId).append(']'); @@ -379,7 +402,7 @@ public final class TestCluster implements Iterable { return null; } - void close() { + public void close() { ensureOpen(); if (this.open.compareAndSet(true, false)) { IOUtils.closeWhileHandlingException(nodes.values()); @@ -554,7 +577,7 @@ public final class TestCluster implements Iterable { /** * This method should be executed before each test to reset the cluster to it's initial state. */ - synchronized void beforeTest(Random random, double transportClientRatio) { + public synchronized void beforeTest(Random random, double transportClientRatio) { reset(random, true, transportClientRatio); } @@ -619,7 +642,7 @@ public final class TestCluster implements Iterable { /** * This method should be executed during tearDown */ - synchronized void afterTest() { + public synchronized void afterTest() { wipeDataDirectories(); resetClients(); /* reset all clients - each test gets its own client based on the Random instance created above. */ diff --git a/src/test/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java b/src/test/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java index 9b90589fe17c..56f2c231fc2d 100644 --- a/src/test/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java +++ b/src/test/java/org/elasticsearch/test/junit/listeners/ReproduceInfoPrinter.java @@ -4,6 +4,7 @@ import com.carrotsearch.randomizedtesting.RandomizedContext; import com.carrotsearch.randomizedtesting.ReproduceErrorMessageBuilder; import com.carrotsearch.randomizedtesting.SeedUtils; import com.carrotsearch.randomizedtesting.TraceFormatting; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.test.ElasticsearchIntegrationTest; @@ -13,9 +14,9 @@ import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import static com.carrotsearch.randomizedtesting.SysGlobals.SYSPROP_ITERATIONS; +import static org.elasticsearch.test.TestCluster.SHARED_CLUSTER_SEED; +import static org.elasticsearch.test.TestCluster.TESTS_CLUSTER_SEED; /** * A {@link RunListener} that emits to {@link System#err} a string with command @@ -46,26 +47,44 @@ public class ReproduceInfoPrinter extends RunListener { final StringBuilder b = new StringBuilder(); b.append("FAILURE : ").append(d.getDisplayName()).append("\n"); b.append("REPRODUCE WITH : mvn test"); - ReproduceErrorMessageBuilder builder = new MavenMessageBuilder(b).appendAllOpts(failure.getDescription()); - if (ElasticsearchIntegrationTest.class.isAssignableFrom(failure.getDescription().getTestClass())) { - builder.appendOpt("tests.cluster_seed", SeedUtils.formatSeed(ElasticsearchIntegrationTest.SHARED_CLUSTER_SEED)); + ReproduceErrorMessageBuilder builder = reproduceErrorMessageBuilder(b).appendAllOpts(failure.getDescription()); + + if (mustAppendClusterSeed(failure)) { + appendClusterSeed(builder); } b.append("\n"); b.append("Throwable:\n"); if (failure.getException() != null) { - TraceFormatting traces = new TraceFormatting(); - try { - traces = RandomizedContext.current().getRunner().getTraceFormatting(); - } catch (IllegalStateException e) { - // Ignore if no context. - } - traces.formatThrowable(b, failure.getException()); + traces().formatThrowable(b, failure.getException()); } + logger.error(b.toString()); } - private static class MavenMessageBuilder extends ReproduceErrorMessageBuilder { + protected boolean mustAppendClusterSeed(Failure failure) { + return ElasticsearchIntegrationTest.class.isAssignableFrom(failure.getDescription().getTestClass()); + } + + protected void appendClusterSeed(ReproduceErrorMessageBuilder builder) { + builder.appendOpt(TESTS_CLUSTER_SEED, SeedUtils.formatSeed(SHARED_CLUSTER_SEED)); + } + + protected ReproduceErrorMessageBuilder reproduceErrorMessageBuilder(StringBuilder b) { + return new MavenMessageBuilder(b); + } + + protected TraceFormatting traces() { + TraceFormatting traces = new TraceFormatting(); + try { + traces = RandomizedContext.current().getRunner().getTraceFormatting(); + } catch (IllegalStateException e) { + // Ignore if no context. + } + return traces; + } + + protected static class MavenMessageBuilder extends ReproduceErrorMessageBuilder { public MavenMessageBuilder(StringBuilder b) { super(b); @@ -80,28 +99,34 @@ public class ReproduceInfoPrinter extends RunListener { /** * Append a single VM option. */ + @Override public ReproduceErrorMessageBuilder appendOpt(String sysPropName, String value) { - if (sysPropName.equals("tests.iters")) { // we don't want the iters to be in there! + if (sysPropName.equals(SYSPROP_ITERATIONS())) { // we don't want the iters to be in there! return this; } - if (value != null && !value.isEmpty()) { + if (Strings.hasLength(value)) { return super.appendOpt(sysPropName, value); } return this; } public ReproduceErrorMessageBuilder appendESProperties() { - for (String sysPropName : Arrays.asList( - "es.logger.level", "es.node.mode", "es.node.local")) { - if (System.getProperty(sysPropName) != null && !System.getProperty(sysPropName).isEmpty()) { - appendOpt(sysPropName, System.getProperty(sysPropName)); - } - } + appendProperties("es.logger.level", "es.node.mode", "es.node.local"); + if (System.getProperty("tests.jvm.argline") != null && !System.getProperty("tests.jvm.argline").isEmpty()) { appendOpt("tests.jvm.argline", "\"" + System.getProperty("tests.jvm.argline") + "\""); } return this; } + protected ReproduceErrorMessageBuilder appendProperties(String... properties) { + for (String sysPropName : properties) { + if (Strings.hasLength(System.getProperty(sysPropName))) { + appendOpt(sysPropName, System.getProperty(sysPropName)); + } + } + return this; + } + } } diff --git a/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTests.java b/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTests.java new file mode 100644 index 000000000000..08860210ca33 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/ElasticsearchRestTests.java @@ -0,0 +1,37 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest; + +import org.elasticsearch.test.rest.junit.RestTestSuiteRunner; +import org.junit.runner.RunWith; + +import static org.apache.lucene.util.LuceneTestCase.Slow; + +/** + * Runs the clients test suite against an elasticsearch node, which can be an external node or an automatically created cluster. + * Communicates with elasticsearch exclusively via REST layer. + * + * @see RestTestSuiteRunner for extensive documentation and all the supported options + */ +@Slow +@RunWith(RestTestSuiteRunner.class) +public class ElasticsearchRestTests { + + +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java b/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java new file mode 100644 index 000000000000..aad2b73fc689 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/RestTestExecutionContext.java @@ -0,0 +1,159 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest; + +import com.google.common.collect.Maps; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.test.rest.client.RestClient; +import org.elasticsearch.test.rest.client.RestException; +import org.elasticsearch.test.rest.client.RestResponse; +import org.elasticsearch.test.rest.spec.RestSpec; + +import java.io.Closeable; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Execution context passed across the REST tests. + * Holds the REST client used to communicate with elasticsearch. + * Caches the last obtained test response and allows to stash part of it within variables + * that can be used as input values in following requests. + */ +public class RestTestExecutionContext implements Closeable { + + private static final ESLogger logger = Loggers.getLogger(RestTestExecutionContext.class); + + private final RestClient restClient; + + private final String esVersion; + + private final Map stash = Maps.newHashMap(); + + private RestResponse response; + + public RestTestExecutionContext(String host, int port, RestSpec restSpec) throws RestException, IOException { + + this.restClient = new RestClient(host, port, restSpec); + this.esVersion = restClient.getEsVersion(); + } + + /** + * Calls an elasticsearch api with the parameters and request body provided as arguments. + * Saves the obtained response in the execution context. + * @throws RestException if the returned status code is non ok + */ + public RestResponse callApi(String apiName, Map params, String body) throws IOException, RestException { + //makes a copy of the parameters before modifying them for this specific request + HashMap requestParams = Maps.newHashMap(params); + for (Map.Entry entry : requestParams.entrySet()) { + if (isStashed(entry.getValue())) { + entry.setValue(unstash(entry.getValue()).toString()); + } + } + try { + return response = callApiInternal(apiName, requestParams, body); + } catch(RestException e) { + response = e.restResponse(); + throw e; + } + } + + /** + * Calls an elasticsearch api internally without saving the obtained response in the context. + * Useful for internal calls (e.g. delete index during teardown) + * @throws RestException if the returned status code is non ok + */ + public RestResponse callApiInternal(String apiName, String... params) throws IOException, RestException { + return restClient.callApi(apiName, params); + } + + private RestResponse callApiInternal(String apiName, Map params, String body) throws IOException, RestException { + return restClient.callApi(apiName, params, body); + } + + /** + * Extracts a specific value from the last saved response + */ + public Object response(String path) throws IOException { + return response.evaluate(path); + } + + /** + * Clears the last obtained response and the stashed fields + */ + public void clear() { + logger.debug("resetting response and stash"); + response = null; + stash.clear(); + } + + /** + * Tells whether a particular value needs to be looked up in the stash + * The stash contains fields eventually extracted from previous responses that can be reused + * as arguments for following requests (e.g. scroll_id) + */ + public boolean isStashed(Object key) { + if (key == null) { + return false; + } + String stashKey = key.toString(); + return Strings.hasLength(stashKey) && stashKey.startsWith("$"); + } + + /** + * Extracts a value from the current stash + * The stash contains fields eventually extracted from previous responses that can be reused + * as arguments for following requests (e.g. scroll_id) + */ + public Object unstash(String value) { + Object stashedValue = stash.get(value.substring(1)); + if (stashedValue == null) { + throw new IllegalArgumentException("stashed value not found for key [" + value + "]"); + } + return stashedValue; + } + + /** + * Allows to saved a specific field in the stash as key-value pair + */ + public void stash(String key, Object value) { + logger.debug("stashing [{}]=[{}]", key, value); + Object old = stash.put(key, value); + if (old != null && old != value) { + logger.trace("replaced stashed value [{}] with same key [{}]", old, key); + } + } + + /** + * Returns the current es version as a string + */ + public String esVersion() { + return esVersion; + } + + /** + * Closes the execution context and releases the underlying resources + */ + public void close() { + this.restClient.close(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/RestClient.java b/src/test/java/org/elasticsearch/test/rest/client/RestClient.java new file mode 100644 index 000000000000..a8ef9ecb7520 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/RestClient.java @@ -0,0 +1,206 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; +import org.elasticsearch.test.rest.client.http.HttpResponse; +import org.elasticsearch.test.rest.spec.RestApi; +import org.elasticsearch.test.rest.spec.RestSpec; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * REST client used to test the elasticsearch REST layer + * Holds the {@link RestSpec} used to translate api calls into REST calls + */ +public class RestClient implements Closeable { + + private static final ESLogger logger = Loggers.getLogger(RestClient.class); + + private final RestSpec restSpec; + private final CloseableHttpClient httpClient; + + private final String host; + private final int port; + + private final String esVersion; + + public RestClient(String host, int port, RestSpec restSpec) throws IOException, RestException { + this.restSpec = restSpec; + this.httpClient = createHttpClient(); + this.host = host; + this.port = port; + this.esVersion = readVersion(); + logger.info("REST client initialized [{}:{}], elasticsearch version: [{}]", host, port, esVersion); + } + + private String readVersion() throws IOException, RestException { + //we make a manual call here without using callApi method, mainly because we are initializing + //and the randomized context doesn't exist for the current thread (would be used to choose the method otherwise) + RestApi restApi = restApi("info"); + assert restApi.getPaths().size() == 1; + assert restApi.getMethods().size() == 1; + RestResponse restResponse = new RestResponse(httpRequestBuilder() + .path(restApi.getPaths().get(0)) + .method(restApi.getMethods().get(0)).execute()); + checkStatusCode(restResponse); + Object version = restResponse.evaluate("version.number"); + if (version == null) { + throw new RuntimeException("elasticsearch version not found in the response"); + } + return version.toString(); + } + + public String getEsVersion() { + return esVersion; + } + + /** + * Calls an api with the provided parameters + * @throws RestException if the obtained status code is non ok, unless the specific error code needs to be ignored + * according to the ignore parameter received as input (which won't get sent to elasticsearch) + */ + public RestResponse callApi(String apiName, String... params) throws IOException, RestException { + if (params.length % 2 != 0) { + throw new IllegalArgumentException("The number of params passed must be even but was [" + params.length + "]"); + } + + Map paramsMap = Maps.newHashMap(); + for (int i = 0; i < params.length; i++) { + paramsMap.put(params[i++], params[i]); + } + + return callApi(apiName, paramsMap, null); + } + + /** + * Calls an api with the provided parameters and body + * @throws RestException if the obtained status code is non ok, unless the specific error code needs to be ignored + * according to the ignore parameter received as input (which won't get sent to elasticsearch) + */ + public RestResponse callApi(String apiName, Map params, String body) throws IOException, RestException { + + List ignores = Lists.newArrayList(); + Map requestParams = null; + if (params != null) { + //makes a copy of the parameters before modifying them for this specific request + requestParams = Maps.newHashMap(params); + //ignore is a special parameter supported by the clients, shouldn't be sent to es + String ignoreString = requestParams.remove("ignore"); + if (Strings.hasLength(ignoreString)) { + try { + ignores.add(Integer.valueOf(ignoreString)); + } catch(NumberFormatException e) { + throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead"); + } + } + } + + HttpRequestBuilder httpRequestBuilder = callApiBuilder(apiName, requestParams, body); + logger.debug("calling api [{}]", apiName); + HttpResponse httpResponse = httpRequestBuilder.execute(); + + //http HEAD doesn't support response body + // For the few api (exists class of api) that use it we need to accept 404 too + if (!httpResponse.supportsBody()) { + ignores.add(404); + } + + RestResponse restResponse = new RestResponse(httpResponse); + checkStatusCode(restResponse, ignores); + return restResponse; + } + + private void checkStatusCode(RestResponse restResponse, List ignores) throws RestException { + //ignore is a catch within the client, to prevent the client from throwing error if it gets non ok codes back + if (ignores.contains(restResponse.getStatusCode())) { + if (logger.isDebugEnabled()) { + logger.debug("ignored non ok status codes {} as requested", ignores); + } + return; + } + checkStatusCode(restResponse); + } + + private void checkStatusCode(RestResponse restResponse) throws RestException { + if (restResponse.isError()) { + throw new RestException("non ok status code [" + restResponse.getStatusCode() + "] returned", restResponse); + } + } + + private HttpRequestBuilder callApiBuilder(String apiName, Map params, String body) { + RestApi restApi = restApi(apiName); + + HttpRequestBuilder httpRequestBuilder = httpRequestBuilder().body(body); + + //divide params between ones that go within query string and ones that go within path + Map pathParts = Maps.newHashMap(); + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + if (restApi.getPathParts().contains(entry.getKey())) { + pathParts.put(entry.getKey(), entry.getValue()); + } else { + httpRequestBuilder.addParam(entry.getKey(), entry.getValue()); + } + } + } + + //the http method is randomized (out of the available ones with the chosen api) + return httpRequestBuilder.method(RandomizedTest.randomFrom(restApi.getSupportedMethods(pathParts.keySet()))) + .path(restApi.getFinalPath(pathParts)); + } + + private RestApi restApi(String apiName) { + RestApi restApi = restSpec.getApi(apiName); + if (restApi == null) { + throw new IllegalArgumentException("rest api [" + apiName + "] doesn't exist in the rest spec"); + } + return restApi; + } + + protected HttpRequestBuilder httpRequestBuilder() { + return new HttpRequestBuilder(httpClient).host(host).port(port); + } + + protected CloseableHttpClient createHttpClient() { + return HttpClients.createDefault(); + } + + /** + * Closes the REST client and the underlying http client + */ + public void close() { + try { + httpClient.close(); + } catch(IOException e) { + logger.error(e.getMessage(), e); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/RestException.java b/src/test/java/org/elasticsearch/test/rest/client/RestException.java new file mode 100644 index 000000000000..5c03f4a2a21e --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/RestException.java @@ -0,0 +1,41 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client; + +/** + * Thrown when a status code that holds an error is received (unless needs to be ignored) + * Holds the original {@link RestResponse} + */ +public class RestException extends Exception { + + private final RestResponse restResponse; + + public RestException(String message, RestResponse restResponse) { + super(message); + this.restResponse = restResponse; + } + + public RestResponse restResponse() { + return restResponse; + } + + public int statusCode() { + return restResponse.getStatusCode(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/RestResponse.java b/src/test/java/org/elasticsearch/test/rest/client/RestResponse.java new file mode 100644 index 000000000000..5e77044f181d --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/RestResponse.java @@ -0,0 +1,88 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client; + +import org.elasticsearch.test.rest.client.http.HttpResponse; +import org.elasticsearch.test.rest.json.JsonPath; + +import java.io.IOException; + +/** + * Response obtained from a REST call + * Supports parsing the response body as json when needed and returning specific values extracted from it + */ +public class RestResponse { + + private final HttpResponse response; + private JsonPath parsedResponse; + + RestResponse(HttpResponse response) { + this.response = response; + } + + public int getStatusCode() { + return response.getStatusCode(); + } + + public String getReasonPhrase() { + return response.getReasonPhrase(); + } + + public String getBody() { + return response.getBody(); + } + + public boolean isError() { + return response.isError(); + } + + /** + * Parses the response body as json and extracts a specific value from it (identified by the provided path) + */ + public Object evaluate(String path) throws IOException { + + if (response == null) { + return null; + } + + JsonPath jsonPath = parsedResponse(); + + if (jsonPath == null) { + //special case: api that don't support body (e.g. exists) return true if 200, false if 404, even if no body + //is_true: '' means the response had no body but the client returned true (caused by 200) + //is_false: '' means the response had no body but the client returned false (caused by 404) + if ("".equals(path) && !response.supportsBody()) { + return !response.isError(); + } + return null; + } + + return jsonPath.evaluate(path); + } + + private JsonPath parsedResponse() throws IOException { + if (parsedResponse != null) { + return parsedResponse; + } + if (response == null || !response.hasBody()) { + return null; + } + return parsedResponse = new JsonPath(response.getBody()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/http/HttpDeleteWithEntity.java b/src/test/java/org/elasticsearch/test/rest/client/http/HttpDeleteWithEntity.java new file mode 100644 index 000000000000..e3ec79c3ebd0 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/http/HttpDeleteWithEntity.java @@ -0,0 +1,40 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client.http; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; + +import java.net.URI; + +/** + * Allows to send DELETE requests providing a body (not supported out of the box) + */ +public class HttpDeleteWithEntity extends HttpEntityEnclosingRequestBase { + + public final static String METHOD_NAME = "DELETE"; + + public HttpDeleteWithEntity(final URI uri) { + setURI(uri); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/http/HttpGetWithEntity.java b/src/test/java/org/elasticsearch/test/rest/client/http/HttpGetWithEntity.java new file mode 100644 index 000000000000..bf4dbfd0ef2e --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/http/HttpGetWithEntity.java @@ -0,0 +1,40 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client.http; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; + +import java.net.URI; + +/** + * Allows to send GET requests providing a body (not supported out of the box) + */ +public class HttpGetWithEntity extends HttpEntityEnclosingRequestBase { + + public final static String METHOD_NAME = "GET"; + + public HttpGetWithEntity(final URI uri) { + setURI(uri); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java b/src/test/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java new file mode 100644 index 000000000000..5c0b84da721d --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java @@ -0,0 +1,186 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client.http; + +import com.google.common.base.Joiner; +import com.google.common.collect.Maps; +import org.apache.http.client.methods.*; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.lucene.util.IOUtils; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * Executable builder for an http request + * Holds an {@link org.apache.http.client.HttpClient} that is used to send the built http request + */ +public class HttpRequestBuilder { + + private static final ESLogger logger = Loggers.getLogger(HttpRequestBuilder.class); + + static final Charset DEFAULT_CHARSET = Charset.forName("utf-8"); + + private final CloseableHttpClient httpClient; + + private String host; + + private int port; + + private String path = ""; + + private final Map params = Maps.newHashMap(); + + private String method = HttpGetWithEntity.METHOD_NAME; + + private String body; + + public HttpRequestBuilder(CloseableHttpClient httpClient) { + this.httpClient = httpClient; + } + + public HttpRequestBuilder host(String host) { + this.host = host; + return this; + } + + public HttpRequestBuilder port(int port) { + this.port = port; + return this; + } + + public HttpRequestBuilder path(String path) { + this.path = path; + return this; + } + + public HttpRequestBuilder addParam(String name, String value) { + this.params.put(name, value); + return this; + } + + public HttpRequestBuilder method(String method) { + this.method = method; + return this; + } + + public HttpRequestBuilder body(String body) { + if (Strings.hasLength(body)) { + this.body = body; + } + return this; + } + + public HttpResponse execute() throws IOException { + CloseableHttpResponse closeableHttpResponse = null; + try { + HttpUriRequest httpUriRequest = buildRequest(); + if (logger.isTraceEnabled()) { + StringBuilder stringBuilder = new StringBuilder(httpUriRequest.getMethod()).append(" ").append(httpUriRequest.getURI()); + if (Strings.hasLength(body)) { + stringBuilder.append("\n").append(body); + } + logger.trace("sending request \n{}", stringBuilder.toString()); + } + closeableHttpResponse = httpClient.execute(httpUriRequest); + HttpResponse httpResponse = new HttpResponse(httpUriRequest, closeableHttpResponse); + logger.trace("got response \n{}\n{}", closeableHttpResponse, httpResponse.hasBody() ? httpResponse.getBody() : ""); + return httpResponse; + } finally { + try { + IOUtils.close(closeableHttpResponse); + } catch (IOException e) { + logger.error("error closing http response", e); + } + } + } + + private HttpUriRequest buildRequest() { + + if (HttpGetWithEntity.METHOD_NAME.equalsIgnoreCase(method)) { + return addOptionalBody(new HttpGetWithEntity(buildUri())); + } + + if (HttpHead.METHOD_NAME.equalsIgnoreCase(method)) { + checkBodyNotSupported(); + return new HttpHead(buildUri()); + } + + if (HttpDeleteWithEntity.METHOD_NAME.equalsIgnoreCase(method)) { + return addOptionalBody(new HttpDeleteWithEntity(buildUri())); + } + + if (HttpPut.METHOD_NAME.equalsIgnoreCase(method)) { + return addOptionalBody(new HttpPut(buildUri())); + } + + if (HttpPost.METHOD_NAME.equalsIgnoreCase(method)) { + return addOptionalBody(new HttpPost(buildUri())); + } + + throw new UnsupportedOperationException("method [" + method + "] not supported"); + } + + private URI buildUri() { + String query; + if (params.size() == 0) { + query = null; + } else { + query = Joiner.on('&').withKeyValueSeparator("=").join(params); + } + try { + return new URI("http", null, host, port, path, query, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + private HttpEntityEnclosingRequestBase addOptionalBody(HttpEntityEnclosingRequestBase requestBase) { + if (Strings.hasText(body)) { + requestBase.setEntity(new StringEntity(body, DEFAULT_CHARSET)); + } + return requestBase; + } + + private void checkBodyNotSupported() { + if (Strings.hasText(body)) { + throw new IllegalArgumentException("request body not supported with head request"); + } + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(method).append(" '") + .append(host).append(":").append(port).append(path).append("'"); + if (!params.isEmpty()) { + stringBuilder.append(", params=").append(params); + } + if (Strings.hasLength(body)) { + stringBuilder.append(", body=\n").append(body); + } + return stringBuilder.toString(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/client/http/HttpResponse.java b/src/test/java/org/elasticsearch/test/rest/client/http/HttpResponse.java new file mode 100644 index 000000000000..538aeeb3bc85 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/client/http/HttpResponse.java @@ -0,0 +1,97 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.client.http; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import java.io.IOException; + +/** + * Response obtained from an http request + * Always consumes the whole response body loading it entirely into a string + */ +public class HttpResponse { + + private static final ESLogger logger = Loggers.getLogger(HttpResponse.class); + + private final HttpUriRequest httpRequest; + private final int statusCode; + private final String reasonPhrase; + private final String body; + + HttpResponse(HttpUriRequest httpRequest, CloseableHttpResponse httpResponse) { + this.httpRequest = httpRequest; + this.statusCode = httpResponse.getStatusLine().getStatusCode(); + this.reasonPhrase = httpResponse.getStatusLine().getReasonPhrase(); + if (httpResponse.getEntity() != null) { + try { + this.body = EntityUtils.toString(httpResponse.getEntity(), HttpRequestBuilder.DEFAULT_CHARSET); + } catch (IOException e) { + EntityUtils.consumeQuietly(httpResponse.getEntity()); + throw new RuntimeException(e); + } finally { + try { + httpResponse.close(); + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + } + } else { + this.body = null; + } + } + + public boolean isError() { + return statusCode >= 400; + } + + public int getStatusCode() { + return statusCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public String getBody() { + return body; + } + + public boolean hasBody() { + return body != null; + } + + public boolean supportsBody() { + return !HttpHead.METHOD_NAME.equals(httpRequest.getMethod()); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(statusCode).append(" ").append(reasonPhrase); + if (hasBody()) { + stringBuilder.append("\n").append(body); + } + return stringBuilder.toString(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/json/JsonPath.java b/src/test/java/org/elasticsearch/test/rest/json/JsonPath.java new file mode 100644 index 000000000000..1d5e86007d69 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/json/JsonPath.java @@ -0,0 +1,111 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.json; + +import com.google.common.collect.Lists; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Holds a json object and allows to extract specific values from it + */ +public class JsonPath { + + final String json; + final Map jsonMap; + + public JsonPath(String json) throws IOException { + this.json = json; + this.jsonMap = convertToMap(json); + } + + private static Map convertToMap(String json) throws IOException { + return JsonXContent.jsonXContent.createParser(json).mapOrderedAndClose(); + } + + /** + * Returns the object corresponding to the provided path if present, null otherwise + */ + public Object evaluate(String path) { + String[] parts = parsePath(path); + Object object = jsonMap; + for (String part : parts) { + object = evaluate(part, object); + if (object == null) { + return null; + } + } + return object; + } + + @SuppressWarnings("unchecked") + private Object evaluate(String key, Object object) { + if (object instanceof Map) { + return ((Map) object).get(key); + } + if (object instanceof List) { + List list = (List) object; + try { + return list.get(Integer.valueOf(key)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("element was a list, but [" + key + "] was not numeric", e); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException("element was a list with " + list.size() + " elements, but [" + key + "] was out of bounds", e); + } + } + + throw new IllegalArgumentException("no object found for [" + key + "] within object of class [" + object.getClass() + "]"); + } + + private String[] parsePath(String path) { + List list = Lists.newArrayList(); + StringBuilder current = new StringBuilder(); + boolean escape = false; + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '\\') { + escape = true; + continue; + } + + if (c == '.') { + if (escape) { + escape = false; + } else { + if (current.length() > 0) { + list.add(current.toString()); + current.setLength(0); + } + continue; + } + } + + current.append(c); + } + + if (current.length() > 0) { + list.add(current.toString()); + } + + return list.toArray(new String[list.size()]); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/junit/DescriptionHelper.java b/src/test/java/org/elasticsearch/test/rest/junit/DescriptionHelper.java new file mode 100644 index 000000000000..0a2179e5ff40 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/junit/DescriptionHelper.java @@ -0,0 +1,79 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.junit; + +import com.google.common.base.Joiner; +import org.elasticsearch.test.rest.section.RestTestSuite; +import org.elasticsearch.test.rest.section.TestSection; +import org.junit.runner.Description; + +import java.util.Map; + +/** + * Helper that knows how to assign proper junit {@link Description}s to each of the node in the tests tree + */ +public final class DescriptionHelper { + + private DescriptionHelper() { + + } + + /* + The following generated ids need to be unique throughout a tests run. + Ids are also shown by IDEs (with junit 4.11 unique ids can be different from what gets shown, not yet in 4.10). + Some tricks are applied to control what gets shown in IDEs in order to keep the ids unique and nice to see at the same time. + */ + + static Description createRootDescription(String name) { + return Description.createSuiteDescription(name); + } + + static Description createApiDescription(String api) { + return Description.createSuiteDescription(api); + } + + static Description createTestSuiteDescription(RestTestSuite restTestSuite) { + //e.g. "indices_open (10_basic)", which leads to 10_basic being returned by Description#getDisplayName + String name = restTestSuite.getApi() + " (" + restTestSuite.getName() + ")"; + return Description.createSuiteDescription(name); + } + + static Description createTestSectionWithRepetitionsDescription(RestTestSuite restTestSuite, TestSection testSection) { + //e.g. "indices_open/10_basic (Basic test for index open/close)", which leads to + //"Basic test for index open/close" being returned by Description#getDisplayName + String name = restTestSuite.getDescription() + " (" + testSection.getName() + ")"; + return Description.createSuiteDescription(name); + } + + static Description createTestSectionIterationDescription(RestTestSuite restTestSuite, TestSection testSection, Map args) { + //e.g. "Basic test for index open/close {#0} (indices_open/10_basic)" some IDEs might strip out the part between parentheses + String name = testSection.getName() + formatMethodArgs(args) + " (" + restTestSuite.getDescription() + ")"; + return Description.createSuiteDescription(name); + } + + private static String formatMethodArgs(Map args) { + if (args == null || args.isEmpty()) return ""; + + StringBuilder b = new StringBuilder(" {"); + Joiner.on(" ").withKeyValueSeparator("").appendTo(b, args); + b.append("}"); + + return b.toString(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/junit/RestReproduceInfoPrinter.java b/src/test/java/org/elasticsearch/test/rest/junit/RestReproduceInfoPrinter.java new file mode 100644 index 000000000000..3f8f8d7d4b96 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/junit/RestReproduceInfoPrinter.java @@ -0,0 +1,112 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.junit; + +import com.carrotsearch.randomizedtesting.ReproduceErrorMessageBuilder; +import com.carrotsearch.randomizedtesting.StandaloneRandomizedContext; +import com.carrotsearch.randomizedtesting.TraceFormatting; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.test.junit.listeners.ReproduceInfoPrinter; +import org.elasticsearch.test.rest.ElasticsearchRestTests; +import org.junit.runner.Description; +import org.junit.runner.notification.Failure; + +import java.util.Arrays; + +import static com.carrotsearch.randomizedtesting.SysGlobals.SYSPROP_RANDOM_SEED; +import static com.carrotsearch.randomizedtesting.SysGlobals.SYSPROP_TESTCLASS; +import static org.elasticsearch.test.rest.junit.RestTestSuiteRunner.*; + +/** + * A {@link org.junit.runner.notification.RunListener} that emits to {@link System#err} a string with command + * line parameters allowing quick REST test re-run under MVN command line. + */ +class RestReproduceInfoPrinter extends ReproduceInfoPrinter { + + protected static final ESLogger logger = Loggers.getLogger(RestReproduceInfoPrinter.class); + + @Override + protected boolean mustAppendClusterSeed(Failure failure) { + return isTestCluster(); + } + + private static boolean isTestCluster() { + return runMode() == RunMode.TEST_CLUSTER; + } + + @Override + protected TraceFormatting traces() { + return new TraceFormatting( + Arrays.asList( + "org.junit.", + "junit.framework.", + "sun.", + "java.lang.reflect.", + "com.carrotsearch.randomizedtesting.", + "org.elasticsearch.test.rest.junit." + )); + } + + @Override + protected ReproduceErrorMessageBuilder reproduceErrorMessageBuilder(StringBuilder b) { + return new MavenMessageBuilder(b); + } + + private static class MavenMessageBuilder extends ReproduceInfoPrinter.MavenMessageBuilder { + + public MavenMessageBuilder(StringBuilder b) { + super(b); + } + + @Override + public ReproduceErrorMessageBuilder appendAllOpts(Description description) { + + try { + appendOpt(SYSPROP_RANDOM_SEED(), StandaloneRandomizedContext.getSeedAsString()); + } catch (IllegalStateException e) { + logger.warn("No context available when dumping reproduce options?"); + } + + //we know that ElasticsearchRestTests is the only one that runs with RestTestSuiteRunner + appendOpt(SYSPROP_TESTCLASS(), ElasticsearchRestTests.class.getName()); + + if (description.getClassName() != null) { + appendOpt(REST_TESTS_SUITE, description.getClassName()); + } + + appendRunnerProperties(); + appendEnvironmentSettings(); + + appendProperties("es.logger.level"); + + if (isTestCluster()) { + appendProperties("es.node.mode", "es.node.local"); + } + + appendRestTestsProperties(); + + return this; + } + + public ReproduceErrorMessageBuilder appendRestTestsProperties() { + return appendProperties(REST_TESTS_MODE, REST_TESTS_SPEC); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/junit/RestTestCandidate.java b/src/test/java/org/elasticsearch/test/rest/junit/RestTestCandidate.java new file mode 100644 index 000000000000..aaf9d10a765c --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/junit/RestTestCandidate.java @@ -0,0 +1,83 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.junit; + +import org.elasticsearch.test.rest.section.RestTestSuite; +import org.elasticsearch.test.rest.section.SetupSection; +import org.elasticsearch.test.rest.section.TestSection; +import org.junit.runner.Description; + +/** + * Wraps {@link org.elasticsearch.test.rest.section.TestSection}s ready to be run, + * properly enriched with the needed execution information. + * The tests tree structure gets flattened to the leaves (test sections) + */ +public class RestTestCandidate { + + private final RestTestSuite restTestSuite; + private final Description suiteDescription; + private final TestSection testSection; + private final Description testDescription; + private final long seed; + + static RestTestCandidate empty(RestTestSuite restTestSuite, Description suiteDescription) { + return new RestTestCandidate(restTestSuite, suiteDescription, null, null, -1); + } + + RestTestCandidate(RestTestSuite restTestSuite, Description suiteDescription, + TestSection testSection, Description testDescription, long seed) { + this.restTestSuite = restTestSuite; + this.suiteDescription = suiteDescription; + this.testSection = testSection; + this.testDescription = testDescription; + this.seed = seed; + } + + public String getApi() { + return restTestSuite.getApi(); + } + + public String getName() { + return restTestSuite.getName(); + } + + public String getSuiteDescription() { + return restTestSuite.getDescription(); + } + + public Description describeSuite() { + return suiteDescription; + } + + public Description describeTest() { + return testDescription; + } + + public SetupSection getSetupSection() { + return restTestSuite.getSetupSection(); + } + + public TestSection getTestSection() { + return testSection; + } + + public long getSeed() { + return seed; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java b/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java new file mode 100644 index 000000000000..453f77dfbab2 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/junit/RestTestSuiteRunner.java @@ -0,0 +1,546 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.junit; + +import com.carrotsearch.hppc.hash.MurmurHash3; +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.Randomness; +import com.carrotsearch.randomizedtesting.SeedUtils; +import com.google.common.collect.Lists; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.transport.InetSocketTransportAddress; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.test.TestCluster; +import org.elasticsearch.test.rest.RestTestExecutionContext; +import org.elasticsearch.test.rest.client.RestException; +import org.elasticsearch.test.rest.client.RestResponse; +import org.elasticsearch.test.rest.parser.RestTestParseException; +import org.elasticsearch.test.rest.parser.RestTestSuiteParser; +import org.elasticsearch.test.rest.section.DoSection; +import org.elasticsearch.test.rest.section.ExecutableSection; +import org.elasticsearch.test.rest.section.RestTestSuite; +import org.elasticsearch.test.rest.section.TestSection; +import org.elasticsearch.test.rest.spec.RestSpec; +import org.elasticsearch.test.rest.support.FileUtils; +import org.junit.runner.Description; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.ParentRunner; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.carrotsearch.randomizedtesting.SeedUtils.parseSeedChain; +import static com.carrotsearch.randomizedtesting.StandaloneRandomizedContext.*; +import static com.carrotsearch.randomizedtesting.SysGlobals.*; +import static org.elasticsearch.test.TestCluster.SHARED_CLUSTER_SEED; +import static org.elasticsearch.test.TestCluster.clusterName; +import static org.elasticsearch.test.rest.junit.DescriptionHelper.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + +/** + * JUnit runner for elasticsearch REST tests + * + * Supports the following options provided as system properties: + * - tests.rest[true|false|host:port]: determines whether the REST tests need to be run and if so + * whether to rely on an external cluster (providing host and port) or fire a test cluster (default) + * - tests.rest.suite: comma separated paths of the test suites to be run (by default loaded from /rest-spec/test) + * it is possible to run only a subset of the tests providing a directory or a single yaml file + * (the default /rest-spec/test prefix is optional when files are loaded from classpath) + * - tests.rest.spec: REST spec path (default /rest-spec/api) + * - tests.iters: runs multiple iterations + * - tests.seed: seed to base the random behaviours on + * - tests.appendseed[true|false]: enables adding the seed to each test section's description (default false) + * - tests.cluster_seed: seed used to create the test cluster (if enabled) + * + */ +public class RestTestSuiteRunner extends ParentRunner { + + private static final ESLogger logger = Loggers.getLogger(RestTestSuiteRunner.class); + + public static final String REST_TESTS_MODE = "tests.rest"; + public static final String REST_TESTS_SUITE = "tests.rest.suite"; + public static final String REST_TESTS_SPEC = "tests.rest.spec"; + + private static final String DEFAULT_TESTS_PATH = "/rest-spec/test"; + private static final String DEFAULT_SPEC_PATH = "/rest-spec/api"; + private static final int DEFAULT_ITERATIONS = 1; + + private static final String PATHS_SEPARATOR = ","; + + private final RestTestExecutionContext restTestExecutionContext; + private final List restTestCandidates; + private final Description rootDescription; + + private final RunMode runMode; + + private final TestCluster testCluster; + + private static final AtomicInteger sequencer = new AtomicInteger(); + + /** The runner's seed (master). */ + private final Randomness runnerRandomness; + + /** + * If {@link com.carrotsearch.randomizedtesting.SysGlobals#SYSPROP_RANDOM_SEED} property is used with two arguments + * (master:test_section) then this field contains test section level override. + */ + private final Randomness testSectionRandomnessOverride; + + enum RunMode { + NO, TEST_CLUSTER, EXTERNAL_CLUSTER + } + + static RunMode runMode() { + String mode = System.getProperty(REST_TESTS_MODE); + if (!Strings.hasLength(mode)) { + //default true: we run the tests starting our own test cluster + mode = Boolean.TRUE.toString(); + } + + if (Boolean.FALSE.toString().equalsIgnoreCase(mode)) { + return RunMode.NO; + } + if (Boolean.TRUE.toString().equalsIgnoreCase(mode)) { + return RunMode.TEST_CLUSTER; + } + return RunMode.EXTERNAL_CLUSTER; + } + + public RestTestSuiteRunner(Class testClass) throws InitializationError { + super(testClass); + + this.runMode = runMode(); + + if (runMode == RunMode.NO) { + //the tests won't be run. the run method will be called anyway but we'll just mark the whole suite as ignored + //no need to go ahead and parse the test suites then + this.runnerRandomness = null; + this.testSectionRandomnessOverride = null; + this.restTestExecutionContext = null; + this.restTestCandidates = null; + this.rootDescription = createRootDescription(getRootSuiteTitle()); + this.rootDescription.addChild(createApiDescription("empty suite")); + this.testCluster = null; + return; + } + + //the REST test suite is supposed to be run only once per jvm against either an external es node or a self started one + if (sequencer.getAndIncrement() > 0) { + throw new InitializationError("only one instance of RestTestSuiteRunner can be created per jvm"); + } + + //either read the seed from system properties (first one in the chain) or generate a new one + final String globalSeed = System.getProperty(SYSPROP_RANDOM_SEED()); + final long initialSeed; + Randomness randomnessOverride = null; + if (Strings.hasLength(globalSeed)) { + final long[] seedChain = parseSeedChain(globalSeed); + if (seedChain.length == 0 || seedChain.length > 2) { + throw new IllegalArgumentException("Invalid system property " + + SYSPROP_RANDOM_SEED() + " specification: " + globalSeed); + } + if (seedChain.length > 1) { + //read the test section level seed if present + randomnessOverride = new Randomness(seedChain[1]); + } + initialSeed = seedChain[0]; + } else { + initialSeed = MurmurHash3.hash(System.nanoTime()); + } + this.runnerRandomness = new Randomness(initialSeed); + this.testSectionRandomnessOverride = randomnessOverride; + logger.info("Master seed: {}", SeedUtils.formatSeed(initialSeed)); + + String host; + int port; + if (runMode == RunMode.TEST_CLUSTER) { + this.testCluster = new TestCluster(SHARED_CLUSTER_SEED, 1, clusterName("REST-tests", ElasticsearchTestCase.CHILD_VM_ID, SHARED_CLUSTER_SEED)); + this.testCluster.beforeTest(runnerRandomness.getRandom(), 0.0f); + HttpServerTransport httpServerTransport = testCluster.getInstance(HttpServerTransport.class); + InetSocketTransportAddress inetSocketTransportAddress = (InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress(); + host = inetSocketTransportAddress.address().getHostName(); + port = inetSocketTransportAddress.address().getPort(); + } else { + this.testCluster = null; + String testsMode = System.getProperty(REST_TESTS_MODE); + String[] split = testsMode.split(":"); + if (split.length < 2) { + throw new InitializationError("address [" + testsMode + "] not valid"); + } + host = split[0]; + try { + port = Integer.valueOf(split[1]); + } catch(NumberFormatException e) { + throw new InitializationError("port is not valid, expected number but was [" + split[1] + "]"); + } + } + + try { + String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH); + RestSpec restSpec = RestSpec.parseFrom(DEFAULT_SPEC_PATH, specPaths); + + this.restTestExecutionContext = new RestTestExecutionContext(host, port, restSpec); + this.rootDescription = createRootDescription(getRootSuiteTitle()); + this.restTestCandidates = collectTestCandidates(rootDescription); + } catch (Throwable e) { + stopTestCluster(); + throw new InitializationError(e); + } + } + + /** + * Parse the test suites and creates the test candidates to be run, together with their junit descriptions. + * The descriptions will be part of a tree containing api/yaml file/test section/eventual multiple iterations. + * The test candidates will be instead flattened out to the leaves level (iterations), the part that needs to be run. + */ + protected List collectTestCandidates(Description rootDescription) + throws RestTestParseException, IOException { + + String[] paths = resolvePathsProperty(REST_TESTS_SUITE, DEFAULT_TESTS_PATH); + Map> yamlSuites = FileUtils.findYamlSuites(DEFAULT_TESTS_PATH, paths); + + int iterations = determineTestSectionIterationCount(); + boolean appendSeedParameter = RandomizedTest.systemPropertyAsBoolean(SYSPROP_APPEND_SEED(), false); + + //we iterate over the files and we shuffle them (grouped by api, and by yaml file) + //meanwhile we create the junit descriptions and test candidates (one per iteration) + + //yaml suites are grouped by directory (effectively by api) + List apis = Lists.newArrayList(yamlSuites.keySet()); + Collections.shuffle(apis, runnerRandomness.getRandom()); + + final boolean fixedSeed = testSectionRandomnessOverride != null; + final boolean hasRepetitions = iterations > 1; + + List testCandidates = Lists.newArrayList(); + RestTestSuiteParser restTestSuiteParser = new RestTestSuiteParser(); + for (String api : apis) { + + Description apiDescription = createApiDescription(api); + rootDescription.addChild(apiDescription); + + List yamlFiles = Lists.newArrayList(yamlSuites.get(api)); + Collections.shuffle(yamlFiles, runnerRandomness.getRandom()); + + for (File yamlFile : yamlFiles) { + RestTestSuite restTestSuite = restTestSuiteParser.parse(restTestExecutionContext.esVersion(), api, yamlFile); + Description testSuiteDescription = createTestSuiteDescription(restTestSuite); + apiDescription.addChild(testSuiteDescription); + + if (restTestSuite.getTestSections().size() == 0) { + assert restTestSuite.getSetupSection().getSkipSection().skipVersion(restTestExecutionContext.esVersion()); + testCandidates.add(RestTestCandidate.empty(restTestSuite, testSuiteDescription)); + continue; + } + + Collections.shuffle(restTestSuite.getTestSections(), runnerRandomness.getRandom()); + + for (TestSection testSection : restTestSuite.getTestSections()) { + + //no need to generate seed if we are going to skip the test section + if (testSection.getSkipSection().skipVersion(restTestExecutionContext.esVersion())) { + Description testSectionDescription = createTestSectionIterationDescription(restTestSuite, testSection, null); + testSuiteDescription.addChild(testSectionDescription); + testCandidates.add(new RestTestCandidate(restTestSuite, testSuiteDescription, testSection, testSectionDescription, -1)); + continue; + } + + Description parentDescription; + if (hasRepetitions) { + //additional level to group multiple iterations under the same test section's node + parentDescription = createTestSectionWithRepetitionsDescription(restTestSuite, testSection); + testSuiteDescription.addChild(parentDescription); + } else { + parentDescription = testSuiteDescription; + } + + final long testSectionSeed = determineTestSectionSeed(restTestSuite.getDescription() + "/" + testSection.getName()); + for (int i = 0; i < iterations; i++) { + //test section name argument needs to be unique here + long thisSeed = (fixedSeed ? testSectionSeed : testSectionSeed ^ MurmurHash3.hash((long) i)); + + final LinkedHashMap args = new LinkedHashMap(); + if (hasRepetitions) { + args.put("#", i); + } + if (hasRepetitions || appendSeedParameter) { + args.put("seed=", SeedUtils.formatSeedChain(runnerRandomness, new Randomness(thisSeed))); + } + + Description testSectionDescription = createTestSectionIterationDescription(restTestSuite, testSection, args); + parentDescription.addChild(testSectionDescription); + testCandidates.add(new RestTestCandidate(restTestSuite, testSuiteDescription, testSection, testSectionDescription, thisSeed)); + } + } + } + } + + return testCandidates; + } + + protected String getRootSuiteTitle() { + if (runMode == RunMode.NO) { + return "elasticsearch REST Tests - not run"; + } + if (runMode == RunMode.TEST_CLUSTER) { + return String.format(Locale.ROOT, "elasticsearch REST Tests - test cluster %s", SeedUtils.formatSeed(SHARED_CLUSTER_SEED)); + } + if (runMode == RunMode.EXTERNAL_CLUSTER) { + return String.format(Locale.ROOT, "elasticsearch REST Tests - external cluster %s", System.getProperty(REST_TESTS_MODE)); + } + throw new UnsupportedOperationException("runMode [" + runMode + "] not supported"); + } + + private int determineTestSectionIterationCount() { + int iterations = RandomizedTest.systemPropertyAsInt(SYSPROP_ITERATIONS(), DEFAULT_ITERATIONS); + if (iterations < 1) { + throw new IllegalArgumentException("System property " + SYSPROP_ITERATIONS() + " must be >= 1 but was [" + iterations + "]"); + } + return iterations; + } + + protected static String[] resolvePathsProperty(String propertyName, String defaultValue) { + String property = System.getProperty(propertyName); + if (!Strings.hasLength(property)) { + return new String[]{defaultValue}; + } else { + return property.split(PATHS_SEPARATOR); + } + } + + /** + * Determine a given test section's initial random seed + */ + private long determineTestSectionSeed(String testSectionName) { + if (testSectionRandomnessOverride != null) { + return getSeed(testSectionRandomnessOverride); + } + + // We assign each test section a different starting hash based on the global seed + // and a hash of their name (so that the order of sections does not matter, only their names) + return getSeed(runnerRandomness) ^ MurmurHash3.hash((long) testSectionName.hashCode()); + } + + @Override + protected List getChildren() { + return restTestCandidates; + } + + @Override + public Description getDescription() { + return rootDescription; + } + + @Override + protected Description describeChild(RestTestCandidate child) { + return child.describeTest(); + } + + @Override + protected Statement classBlock(RunNotifier notifier) { + //we remove support for @BeforeClass & @AfterClass and JUnit Rules (as we don't call super) + Statement statement = childrenInvoker(notifier); + statement = withExecutionContextClose(statement); + if (testCluster != null) { + return withTestClusterClose(statement); + } + return statement; + } + + protected Statement withExecutionContextClose(Statement statement) { + return new RunAfter(statement, new Statement() { + @Override + public void evaluate() throws Throwable { + restTestExecutionContext.close(); + } + }); + } + + protected Statement withTestClusterClose(Statement statement) { + return new RunAfter(statement, new Statement() { + @Override + public void evaluate() throws Throwable { + stopTestCluster(); + } + }); + } + + @Override + public void run(final RunNotifier notifier) { + + if (runMode == RunMode.NO) { + notifier.fireTestIgnored(rootDescription.getChildren().get(0)); + return; + } + + notifier.addListener(new RestReproduceInfoPrinter()); + + //the test suite gets run on a separate thread as the randomized context is per thread + //once the randomized context is disposed it's not possible to create it again on the same thread + final Thread thread = new Thread() { + @Override + public void run() { + try { + createRandomizedContext(getTestClass().getJavaClass(), runnerRandomness); + RestTestSuiteRunner.super.run(notifier); + } finally { + disposeRandomizedContext(); + } + } + }; + + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + notifier.fireTestFailure(new Failure(getDescription(), + new RuntimeException("Interrupted while waiting for the suite runner? Weird.", e))); + } + } + + @Override + protected void runChild(RestTestCandidate testCandidate, RunNotifier notifier) { + + //if the while suite needs to be skipped, no test sections were loaded, only an empty one that we need to mark as ignored + if (testCandidate.getSetupSection().getSkipSection().skipVersion(restTestExecutionContext.esVersion())) { + logger.info("skipped test suite [{}]\nreason: {}\nskip versions: {} (current version: {})", + testCandidate.getSuiteDescription(), testCandidate.getSetupSection().getSkipSection().getReason(), + testCandidate.getSetupSection().getSkipSection().getVersion(), restTestExecutionContext.esVersion()); + + notifier.fireTestIgnored(testCandidate.describeSuite()); + return; + } + + //from now on no more empty test candidates are expected + assert testCandidate.getTestSection() != null; + + if (testCandidate.getTestSection().getSkipSection().skipVersion(restTestExecutionContext.esVersion())) { + logger.info("skipped test [{}/{}]\nreason: {}\nskip versions: {} (current version: {})", + testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName(), + testCandidate.getTestSection().getSkipSection().getReason(), + testCandidate.getTestSection().getSkipSection().getVersion(), restTestExecutionContext.esVersion()); + + notifier.fireTestIgnored(testCandidate.describeTest()); + return; + } + + runLeaf(methodBlock(testCandidate), testCandidate.describeTest(), notifier); + } + + protected Statement methodBlock(final RestTestCandidate testCandidate) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final String testThreadName = "TEST-" + testCandidate.getSuiteDescription() + + "." + testCandidate.getTestSection().getName() + "-seed#" + SeedUtils.formatSeedChain(runnerRandomness); + // This has a side effect of setting up a nested context for the test thread. + final String restoreName = Thread.currentThread().getName(); + try { + Thread.currentThread().setName(testThreadName); + pushRandomness(new Randomness(testCandidate.getSeed())); + runTestSection(testCandidate); + } finally { + Thread.currentThread().setName(restoreName); + popAndDestroy(); + } + } + }; + } + + protected void runTestSection(RestTestCandidate testCandidate) + throws IOException, RestException { + + //let's check that there is something to run, otherwise there might be a problem with the test section + if (testCandidate.getTestSection().getExecutableSections().size() == 0) { + throw new IllegalArgumentException("No executable sections loaded for [" + + testCandidate.getSuiteDescription() + "/" + testCandidate.getTestSection().getName() + "]"); + } + + logger.info("cleaning up before test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + tearDown(); + + logger.info("start test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + + if (!testCandidate.getSetupSection().isEmpty()) { + logger.info("start setup test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + for (DoSection doSection : testCandidate.getSetupSection().getDoSections()) { + doSection.execute(restTestExecutionContext); + } + logger.info("end setup test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + } + + restTestExecutionContext.clear(); + + for (ExecutableSection executableSection : testCandidate.getTestSection().getExecutableSections()) { + executableSection.execute(restTestExecutionContext); + } + + logger.info("end test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + + logger.info("cleaning up after test [{}: {}]", testCandidate.getSuiteDescription(), testCandidate.getTestSection().getName()); + tearDown(); + } + + private void tearDown() throws IOException, RestException { + wipeIndices(); + wipeTemplates(); + restTestExecutionContext.clear(); + } + + private void wipeIndices() throws IOException, RestException { + logger.debug("deleting all indices"); + RestResponse restResponse = restTestExecutionContext.callApiInternal("indices.delete", "index", "_all"); + assertThat(restResponse.getStatusCode(), equalTo(200)); + } + + @SuppressWarnings("unchecked") + public void wipeTemplates() throws IOException, RestException { + logger.debug("deleting all templates"); + //delete templates by wildcard was only added in 0.90.6 + //httpResponse = restTestExecutionContext.callApi("indices.delete_template", "name", "*"); + RestResponse restResponse = restTestExecutionContext.callApiInternal("cluster.state", "filter_nodes", "true", + "filter_routing_table", "true", "filter_blocks", "true"); + assertThat(restResponse.getStatusCode(), equalTo(200)); + Object object = restResponse.evaluate("metadata.templates"); + assertThat(object, instanceOf(Map.class)); + Set templates = ((Map) object).keySet(); + for (String template : templates) { + restResponse = restTestExecutionContext.callApiInternal("indices.delete_template", "name", template); + assertThat(restResponse.getStatusCode(), equalTo(200)); + } + } + + private void stopTestCluster() { + if (runMode == RunMode.TEST_CLUSTER) { + assert testCluster != null; + testCluster.afterTest(); + testCluster.close(); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/junit/RunAfter.java b/src/test/java/org/elasticsearch/test/rest/junit/RunAfter.java new file mode 100644 index 000000000000..abd1b2b98c7c --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/junit/RunAfter.java @@ -0,0 +1,56 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.junit; + +import com.google.common.collect.Lists; +import org.junit.runners.model.MultipleFailureException; +import org.junit.runners.model.Statement; + +import java.util.List; + +/** + * {@link Statement} that allows to run a specific statement after another one + */ +public class RunAfter extends Statement { + + private final Statement next; + private final Statement after; + + public RunAfter(Statement next, Statement after) { + this.next = next; + this.after = after; + } + + @Override + public void evaluate() throws Throwable { + List errors = Lists.newArrayList(); + try { + next.evaluate(); + } catch (Throwable e) { + errors.add(e); + } finally { + try { + after.evaluate(); + } catch (Throwable e) { + errors.add(e); + } + } + MultipleFailureException.assertEmpty(errors); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/DoSectionParser.java b/src/test/java/org/elasticsearch/test/rest/parser/DoSectionParser.java new file mode 100644 index 000000000000..cdbfe0fe9f2e --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/DoSectionParser.java @@ -0,0 +1,86 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.ApiCallSection; +import org.elasticsearch.test.rest.section.DoSection; + +import java.io.IOException; +import java.util.Map; + +/** + * Parser for do sections + */ +public class DoSectionParser implements RestTestFragmentParser { + + @Override + public DoSection parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + + XContentParser parser = parseContext.parser(); + + String currentFieldName = null; + XContentParser.Token token; + + DoSection doSection = new DoSection(); + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if ("catch".equals(currentFieldName)) { + doSection.setCatch(parser.text()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (currentFieldName != null) { + ApiCallSection apiCallSection = new ApiCallSection(currentFieldName); + String paramName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + paramName = parser.currentName(); + } else if (token.isValue()) { + if ("body".equals(paramName)) { + apiCallSection.addBody(parser.text()); + } else { + apiCallSection.addParam(paramName, parser.text()); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if ("body".equals(paramName)) { + Map map = parser.mapOrdered(); + XContentBuilder contentBuilder = XContentFactory.jsonBuilder().map(map); + apiCallSection.addBody(contentBuilder.string()); + } + } + } + doSection.setApiCallSection(apiCallSection); + } + } + } + + parser.nextToken(); + + if (doSection.getApiCallSection() == null) { + throw new RestTestParseException("client call section is mandatory within a do section"); + } + + return doSection; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/GreaterThanParser.java b/src/test/java/org/elasticsearch/test/rest/parser/GreaterThanParser.java new file mode 100644 index 000000000000..3ca5214346f9 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/GreaterThanParser.java @@ -0,0 +1,39 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.test.rest.section.GreaterThanAssertion; + +import java.io.IOException; + +/** + * Parser for gt assert sections + */ +public class GreaterThanParser implements RestTestFragmentParser { + + @Override + public GreaterThanAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + Tuple stringObjectTuple = parseContext.parseTuple(); + if (! (stringObjectTuple.v2() instanceof Comparable) ) { + throw new RestTestParseException("gt section can only be used with objects that support natural ordering, found " + stringObjectTuple.v2().getClass().getSimpleName()); + } + return new GreaterThanAssertion(stringObjectTuple.v1(), stringObjectTuple.v2()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/IsFalseParser.java b/src/test/java/org/elasticsearch/test/rest/parser/IsFalseParser.java new file mode 100644 index 000000000000..2c113a874a4a --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/IsFalseParser.java @@ -0,0 +1,34 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.test.rest.section.IsFalseAssertion; + +import java.io.IOException; + +/** + * Parser for is_false assert sections + */ +public class IsFalseParser implements RestTestFragmentParser { + + @Override + public IsFalseAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + return new IsFalseAssertion(parseContext.parseField()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/IsTrueParser.java b/src/test/java/org/elasticsearch/test/rest/parser/IsTrueParser.java new file mode 100644 index 000000000000..37e5cb2cfebd --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/IsTrueParser.java @@ -0,0 +1,34 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.test.rest.section.IsTrueAssertion; + +import java.io.IOException; + +/** + * Parser for is_true assert sections + */ +public class IsTrueParser implements RestTestFragmentParser { + + @Override + public IsTrueAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + return new IsTrueAssertion(parseContext.parseField()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/LengthParser.java b/src/test/java/org/elasticsearch/test/rest/parser/LengthParser.java new file mode 100644 index 000000000000..63ce031a47e6 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/LengthParser.java @@ -0,0 +1,48 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.test.rest.section.LengthAssertion; + +import java.io.IOException; + +/** + * Parser for length assert sections + */ +public class LengthParser implements RestTestFragmentParser { + + @Override + public LengthAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + Tuple stringObjectTuple = parseContext.parseTuple(); + assert stringObjectTuple.v2() != null; + int value; + if (stringObjectTuple.v2() instanceof Number) { + value = ((Number) stringObjectTuple.v2()).intValue(); + } else { + try { + value = Integer.valueOf(stringObjectTuple.v2().toString()); + } catch(NumberFormatException e) { + throw new RestTestParseException("length is not a valid number", e); + } + + } + return new LengthAssertion(stringObjectTuple.v1(), value); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/LessThanParser.java b/src/test/java/org/elasticsearch/test/rest/parser/LessThanParser.java new file mode 100644 index 000000000000..245989e3599b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/LessThanParser.java @@ -0,0 +1,39 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.test.rest.section.LessThanAssertion; + +import java.io.IOException; + +/** + * Parser for lt assert sections + */ +public class LessThanParser implements RestTestFragmentParser { + + @Override + public LessThanAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + Tuple stringObjectTuple = parseContext.parseTuple(); + if (! (stringObjectTuple.v2() instanceof Comparable) ) { + throw new RestTestParseException("lt section can only be used with objects that support natural ordering, found " + stringObjectTuple.v2().getClass().getSimpleName()); + } + return new LessThanAssertion(stringObjectTuple.v1(), stringObjectTuple.v2()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/MatchParser.java b/src/test/java/org/elasticsearch/test/rest/parser/MatchParser.java new file mode 100644 index 000000000000..4c3498909175 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/MatchParser.java @@ -0,0 +1,36 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.test.rest.section.MatchAssertion; + +import java.io.IOException; + +/** + * Parser for match assert sections + */ +public class MatchParser implements RestTestFragmentParser { + + @Override + public MatchAssertion parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + Tuple stringObjectTuple = parseContext.parseTuple(); + return new MatchAssertion(stringObjectTuple.v1(), stringObjectTuple.v2()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/RestTestFragmentParser.java b/src/test/java/org/elasticsearch/test/rest/parser/RestTestFragmentParser.java new file mode 100644 index 000000000000..0528b8e9fdf9 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/RestTestFragmentParser.java @@ -0,0 +1,33 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import java.io.IOException; + +/** + * Base parser for a REST test suite fragment + * @param the test fragment's type that gets parsed and returned + */ +public interface RestTestFragmentParser { + + /** + * Parses a test fragment given the current {@link RestTestSuiteParseContext} + */ + T parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException; +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/RestTestParseException.java b/src/test/java/org/elasticsearch/test/rest/parser/RestTestParseException.java new file mode 100644 index 000000000000..4ad0b76840f6 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/RestTestParseException.java @@ -0,0 +1,33 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +/** + * Exception thrown whenever there is a problem parsing any of the REST test suite fragment + */ +public class RestTestParseException extends Exception { + + RestTestParseException(String message) { + super(message); + } + + RestTestParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/RestTestSectionParser.java b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSectionParser.java new file mode 100644 index 000000000000..6689ac6cefdc --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSectionParser.java @@ -0,0 +1,66 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.TestSection; + +import java.io.IOException; + +/** + * Parser for a complete test section + * + * Depending on the elasticsearch version the tests are going to run against, test sections might need to get skipped + * In that case the relevant test sections parsing is entirely skipped + */ +public class RestTestSectionParser implements RestTestFragmentParser { + + @Override + public TestSection parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + XContentParser parser = parseContext.parser(); + parseContext.advanceToFieldName(); + TestSection testSection = new TestSection(parser.currentName()); + parser.nextToken(); + testSection.setSkipSection(parseContext.parseSkipSection()); + + boolean skip = testSection.getSkipSection().skipVersion(parseContext.getCurrentVersion()); + + while ( parser.currentToken() != XContentParser.Token.END_ARRAY) { + if (skip) { + //if there was a skip section, there was a setup section as well, which means that we are sure + // the current token is at the beginning of a new object + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + //we need to be at the beginning of an object to be able to skip children + parser.skipChildren(); + //after skipChildren we are at the end of the skipped object, need to move on + parser.nextToken(); + } else { + parseContext.advanceToFieldName(); + testSection.addExecutableSection(parseContext.parseExecutableSection()); + } + } + + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.END_OBJECT; + parser.nextToken(); + + return testSection; + } + +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParseContext.java b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParseContext.java new file mode 100644 index 000000000000..1b4f868e77e1 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParseContext.java @@ -0,0 +1,165 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import com.google.common.collect.Maps; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.*; + +import java.io.IOException; +import java.util.Map; + +/** + * Context shared across the whole tests parse phase. + * Provides shared parse methods and holds information needed to parse the test sections (e.g. es version) + */ +public class RestTestSuiteParseContext { + + private static final SetupSectionParser SETUP_SECTION_PARSER = new SetupSectionParser(); + private static final RestTestSectionParser TEST_SECTION_PARSER = new RestTestSectionParser(); + private static final SkipSectionParser SKIP_SECTION_PARSER = new SkipSectionParser(); + private static final DoSectionParser DO_SECTION_PARSER = new DoSectionParser(); + private static final Map> EXECUTABLE_SECTIONS_PARSERS = Maps.newHashMap(); + static { + EXECUTABLE_SECTIONS_PARSERS.put("do", DO_SECTION_PARSER); + EXECUTABLE_SECTIONS_PARSERS.put("set", new SetSectionParser()); + EXECUTABLE_SECTIONS_PARSERS.put("match", new MatchParser()); + EXECUTABLE_SECTIONS_PARSERS.put("is_true", new IsTrueParser()); + EXECUTABLE_SECTIONS_PARSERS.put("is_false", new IsFalseParser()); + EXECUTABLE_SECTIONS_PARSERS.put("gt", new GreaterThanParser()); + EXECUTABLE_SECTIONS_PARSERS.put("lt", new LessThanParser()); + EXECUTABLE_SECTIONS_PARSERS.put("length", new LengthParser()); + } + + private final String api; + private final String suiteName; + private final XContentParser parser; + private final String currentVersion; + + public RestTestSuiteParseContext(String api, String suiteName, XContentParser parser, String currentVersion) { + this.api = api; + this.suiteName = suiteName; + this.parser = parser; + this.currentVersion = currentVersion; + } + + public String getApi() { + return api; + } + + public String getSuiteName() { + return suiteName; + } + + public XContentParser parser() { + return parser; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public SetupSection parseSetupSection() throws IOException, RestTestParseException { + + advanceToFieldName(); + + if ("setup".equals(parser.currentName())) { + parser.nextToken(); + SetupSection setupSection = SETUP_SECTION_PARSER.parse(this); + parser.nextToken(); + return setupSection; + } + + return SetupSection.EMPTY; + } + + public TestSection parseTestSection() throws IOException, RestTestParseException { + return TEST_SECTION_PARSER.parse(this); + } + + public SkipSection parseSkipSection() throws IOException, RestTestParseException { + + advanceToFieldName(); + + if ("skip".equals(parser.currentName())) { + SkipSection skipSection = SKIP_SECTION_PARSER.parse(this); + parser.nextToken(); + return skipSection; + } + + return SkipSection.EMPTY; + } + + public ExecutableSection parseExecutableSection() throws IOException, RestTestParseException { + advanceToFieldName(); + String section = parser.currentName(); + RestTestFragmentParser execSectionParser = EXECUTABLE_SECTIONS_PARSERS.get(section); + if (execSectionParser == null) { + throw new RestTestParseException("no parser found for executable section [" + section + "]"); + } + ExecutableSection executableSection = execSectionParser.parse(this); + parser.nextToken(); + return executableSection; + } + + public DoSection parseDoSection() throws IOException, RestTestParseException { + return DO_SECTION_PARSER.parse(this); + } + + public void advanceToFieldName() throws IOException, RestTestParseException { + XContentParser.Token token = parser.currentToken(); + //we are in the beginning, haven't called nextToken yet + if (token == null) { + token = parser.nextToken(); + } + if (token == XContentParser.Token.START_ARRAY) { + token = parser.nextToken(); + } + if (token == XContentParser.Token.START_OBJECT) { + token = parser.nextToken(); + } + if (token != XContentParser.Token.FIELD_NAME) { + throw new RestTestParseException("malformed test section: field suiteName expected but found " + token); + } + } + + public String parseField() throws IOException, RestTestParseException { + parser.nextToken(); + assert parser.currentToken().isValue(); + String field = parser.text(); + parser.nextToken(); + return field; + } + + public Tuple parseTuple() throws IOException, RestTestParseException { + parser.nextToken(); + advanceToFieldName(); + Map map = parser.map(); + assert parser.currentToken() == XContentParser.Token.END_OBJECT; + parser.nextToken(); + + if (map.size() != 1) { + throw new RestTestParseException("expected key value pair but found " + map.size() + " "); + } + + Map.Entry entry = map.entrySet().iterator().next(); + return Tuple.tuple(entry.getKey(), entry.getValue()); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParser.java b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParser.java new file mode 100644 index 000000000000..3d0203424be5 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/RestTestSuiteParser.java @@ -0,0 +1,96 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.section.RestTestSuite; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * Parser for a complete test suite (yaml file) + * + * Depending on the elasticsearch version the tests are going to run against, a whole test suite might need to get skipped + * In that case the relevant test sections parsing is entirely skipped + */ +public class RestTestSuiteParser implements RestTestFragmentParser { + + public RestTestSuite parse(String currentVersion, String api, File file) throws IOException, RestTestParseException { + + if (!file.isFile()) { + throw new IllegalArgumentException(file.getAbsolutePath() + " is not a file"); + } + + XContentParser parser = YamlXContent.yamlXContent.createParser(new FileInputStream(file)); + try { + String filename = file.getName(); + //remove the file extension + int i = filename.lastIndexOf('.'); + if (i > 0) { + filename = filename.substring(0, i); + } + RestTestSuiteParseContext testParseContext = new RestTestSuiteParseContext(api, filename, parser, currentVersion); + return parse(testParseContext); + } finally { + parser.close(); + } + } + + @Override + public RestTestSuite parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + XContentParser parser = parseContext.parser(); + + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + + RestTestSuite restTestSuite = new RestTestSuite(parseContext.getApi(), parseContext.getSuiteName()); + + restTestSuite.setSetupSection(parseContext.parseSetupSection()); + + boolean skip = restTestSuite.getSetupSection().getSkipSection().skipVersion(parseContext.getCurrentVersion()); + + while(true) { + //the "---" section separator is not understood by the yaml parser. null is returned, same as when the parser is closed + //we need to somehow distinguish between a null in the middle of a test ("---") + // and a null at the end of the file (at least two consecutive null tokens) + if(parser.currentToken() == null) { + if (parser.nextToken() == null) { + break; + } + } + + if (skip) { + //if there was a skip section, there was a setup section as well, which means that we are sure + // the current token is at the beginning of a new object + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + //we need to be at the beginning of an object to be able to skip children + parser.skipChildren(); + //after skipChildren we are at the end of the skipped object, need to move on + parser.nextToken(); + } else { + restTestSuite.addTestSection(parseContext.parseTestSection()); + } + } + + return restTestSuite; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/SetSectionParser.java b/src/test/java/org/elasticsearch/test/rest/parser/SetSectionParser.java new file mode 100644 index 000000000000..1caa96d9972c --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/SetSectionParser.java @@ -0,0 +1,57 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.SetSection; + +import java.io.IOException; + +/** + * Parser for set sections + */ +public class SetSectionParser implements RestTestFragmentParser { + + @Override + public SetSection parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + + XContentParser parser = parseContext.parser(); + + String currentFieldName = null; + XContentParser.Token token; + + SetSection setSection = new SetSection(); + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + setSection.addSet(currentFieldName, parser.text()); + } + } + + parser.nextToken(); + + if (setSection.getStash().isEmpty()) { + throw new RestTestParseException("set section must set at least a value"); + } + + return setSection; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/SetupSectionParser.java b/src/test/java/org/elasticsearch/test/rest/parser/SetupSectionParser.java new file mode 100644 index 000000000000..8e0f22930601 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/SetupSectionParser.java @@ -0,0 +1,66 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.SetupSection; + +import java.io.IOException; + +/** + * Parser for setup sections + */ +public class SetupSectionParser implements RestTestFragmentParser { + + @Override + public SetupSection parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + + XContentParser parser = parseContext.parser(); + + SetupSection setupSection = new SetupSection(); + setupSection.setSkipSection(parseContext.parseSkipSection()); + + boolean skip = setupSection.getSkipSection().skipVersion(parseContext.getCurrentVersion()); + + while (parser.currentToken() != XContentParser.Token.END_ARRAY) { + if (skip) { + //if there was a skip section, there was a setup section as well, which means that we are sure + // the current token is at the beginning of a new object + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + //we need to be at the beginning of an object to be able to skip children + parser.skipChildren(); + //after skipChildren we are at the end of the skipped object, need to move on + parser.nextToken(); + } else { + parseContext.advanceToFieldName(); + if (!"do".equals(parser.currentName())) { + throw new RestTestParseException("section [" + parser.currentName() + "] not supported within setup section"); + } + + parser.nextToken(); + setupSection.addDoSection(parseContext.parseDoSection()); + parser.nextToken(); + } + } + + parser.nextToken(); + + return setupSection; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/parser/SkipSectionParser.java b/src/test/java/org/elasticsearch/test/rest/parser/SkipSectionParser.java new file mode 100644 index 000000000000..7c8d58389e44 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/parser/SkipSectionParser.java @@ -0,0 +1,68 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.parser; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.rest.section.SkipSection; + +import java.io.IOException; + +/** + * Parser for skip sections + */ +public class SkipSectionParser implements RestTestFragmentParser { + + @Override + public SkipSection parse(RestTestSuiteParseContext parseContext) throws IOException, RestTestParseException { + + XContentParser parser = parseContext.parser(); + + String currentFieldName = null; + XContentParser.Token token; + String version = null; + String reason = null; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if ("version".equals(currentFieldName)) { + version = parser.text(); + } else if ("reason".equals(currentFieldName)) { + reason = parser.text(); + } else { + throw new RestTestParseException("field " + currentFieldName + " not supported within skip section"); + } + } + } + + parser.nextToken(); + + if (!Strings.hasLength(version)) { + throw new RestTestParseException("version is mandatory within skip section"); + } + if (!Strings.hasLength(reason)) { + throw new RestTestParseException("reason is mandatory within skip section"); + } + + return new SkipSection(version, reason); + + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/ApiCallSection.java b/src/test/java/org/elasticsearch/test/rest/section/ApiCallSection.java new file mode 100644 index 000000000000..159d750eea4f --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/ApiCallSection.java @@ -0,0 +1,89 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import java.util.List; +import java.util.Map; + +/** + * Represents a test fragment that contains the information needed to call an api + */ +public class ApiCallSection { + + private final String api; + private final Map params = Maps.newHashMap(); + private final List bodies = Lists.newArrayList(); + + private static final String EMPTY_BODY = ""; + + public ApiCallSection(String api) { + this.api = api; + } + + public String getApi() { + return api; + } + + public Map getParams() { + //make sure we never modify the parameters once returned + return ImmutableMap.copyOf(params); + } + + public void addParam(String key, String value) { + String existingValue = params.get(key); + if (existingValue != null) { + value = Joiner.on(",").join(existingValue, value); + } + this.params.put(key, value); + } + + public List getBodiesAsList() { + return ImmutableList.copyOf(bodies); + } + + public String getBody() { + if (bodies.size() == 0) { + return EMPTY_BODY; + } + + if (bodies.size() == 1) { + return bodies.get(0); + } + + StringBuilder bodyBuilder = new StringBuilder(); + for (String body : bodies) { + bodyBuilder.append(body).append("\n"); + } + return bodyBuilder.toString(); + } + + public void addBody(String body) { + this.bodies.add(body); + } + + public boolean hasBody() { + return bodies.size() > 0; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/Assertion.java b/src/test/java/org/elasticsearch/test/rest/section/Assertion.java new file mode 100644 index 000000000000..d719623011ac --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/Assertion.java @@ -0,0 +1,66 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.test.rest.RestTestExecutionContext; + +import java.io.IOException; + +/** + * Base class for executable sections that hold assertions + */ +public abstract class Assertion implements ExecutableSection { + + private final String field; + private final Object expectedValue; + + protected Assertion(String field, Object expectedValue) { + this.field = field; + this.expectedValue = expectedValue; + } + + public final String getField() { + return field; + } + + public final Object getExpectedValue() { + return expectedValue; + } + + protected final Object resolveExpectedValue(RestTestExecutionContext executionContext) { + if (executionContext.isStashed(expectedValue)) { + return executionContext.unstash(expectedValue.toString()); + } + return expectedValue; + } + + protected final Object getActualValue(RestTestExecutionContext executionContext) throws IOException { + return executionContext.response(field); + } + + @Override + public final void execute(RestTestExecutionContext executionContext) throws IOException { + doAssert(getActualValue(executionContext), resolveExpectedValue(executionContext)); + } + + /** + * Executes the assertion comparing the actual value (parsed from the response) with the expected one + */ + protected abstract void doAssert(Object actualValue, Object expectedValue); +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/DoSection.java b/src/test/java/org/elasticsearch/test/rest/section/DoSection.java new file mode 100644 index 000000000000..c528a0b8bb29 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/DoSection.java @@ -0,0 +1,116 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.test.rest.RestTestExecutionContext; +import org.elasticsearch.test.rest.client.RestException; +import org.elasticsearch.test.rest.client.RestResponse; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Represents a do section: + * + * - do: + * catch: missing + * update: + * index: test_1 + * type: test + * id: 1 + * body: { doc: { foo: bar } } + * + */ +public class DoSection implements ExecutableSection { + + private static final ESLogger logger = Loggers.getLogger(DoSection.class); + + private String catchParam; + private ApiCallSection apiCallSection; + + public String getCatch() { + return catchParam; + } + + public void setCatch(String catchParam) { + this.catchParam = catchParam; + } + + public ApiCallSection getApiCallSection() { + return apiCallSection; + } + + public void setApiCallSection(ApiCallSection apiCallSection) { + this.apiCallSection = apiCallSection; + } + + @Override + public void execute(RestTestExecutionContext executionContext) throws IOException { + + try { + executionContext.callApi(apiCallSection.getApi(), apiCallSection.getParams(), apiCallSection.getBody()); + + } catch(RestException e) { + if (!Strings.hasLength(catchParam)) { + fail(formatStatusCodeMessage(e.restResponse(), "2xx")); + } + + if ("param".equals(catchParam)) { + //client should throw validation error before sending request + //lets just return without doing anything as we don't have any client to test here + logger.info("found [catch: param], no request sent"); + } else if ("missing".equals(catchParam)) { + assertThat(formatStatusCodeMessage(e.restResponse(), "404"), e.statusCode(), equalTo(404)); + } else if ("conflict".equals(catchParam)) { + assertThat(formatStatusCodeMessage(e.restResponse(), "409"), e.statusCode(), equalTo(409)); + } else if ("forbidden".equals(catchParam)) { + assertThat(formatStatusCodeMessage(e.restResponse(), "403"), e.statusCode(), equalTo(403)); + } else if ("request".equals(catchParam)) { + //generic error response from ES + assertThat(formatStatusCodeMessage(e.restResponse(), "4xx|5xx"), e.statusCode(), greaterThanOrEqualTo(400)); + } else if (catchParam.startsWith("/") && catchParam.endsWith("/")) { + //the text of the error message matches regular expression + assertThat(formatStatusCodeMessage(e.restResponse(), "4xx|5xx"), e.statusCode(), greaterThanOrEqualTo(400)); + Object error = executionContext.response("error"); + assertThat("error was expected in the response", error, notNullValue()); + //remove delimiters from regex + String regex = catchParam.substring(1, catchParam.length() - 1); + String errorMessage = error.toString(); + Matcher matcher = Pattern.compile(regex).matcher(errorMessage); + assertThat("error message [" + errorMessage + "] was expected to match [" + catchParam + "] but didn't", + matcher.find(), equalTo(true)); + } else { + throw new UnsupportedOperationException("catch value [" + catchParam + "] not supported"); + } + } + } + + private String formatStatusCodeMessage(RestResponse restResponse, String expected) { + return "expected [" + expected + "] status code but api [" + apiCallSection.getApi() + "] returned [" + + restResponse.getStatusCode() + " " + restResponse.getReasonPhrase() + "] [" + restResponse.getBody() + "]"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/ExecutableSection.java b/src/test/java/org/elasticsearch/test/rest/section/ExecutableSection.java new file mode 100644 index 000000000000..5044bcdba6bf --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/ExecutableSection.java @@ -0,0 +1,34 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.test.rest.RestTestExecutionContext; + +import java.io.IOException; + +/** + * Represents a test fragment that can be executed (e.g. api call, assertion) + */ +public interface ExecutableSection { + + /** + * Executes the section passing in the execution context + */ + void execute(RestTestExecutionContext executionContext) throws IOException; +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/GreaterThanAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/GreaterThanAssertion.java new file mode 100644 index 000000000000..95efa9013075 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/GreaterThanAssertion.java @@ -0,0 +1,54 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + +/** + * Represents a gt assert section: + * + * - gt: { fields._ttl: 0} + * + */ +public class GreaterThanAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(GreaterThanAssertion.class); + + public GreaterThanAssertion(String field, Object expectedValue) { + super(field, expectedValue); + } + + @Override + @SuppressWarnings("unchecked") + protected void doAssert(Object actualValue, Object expectedValue) { + logger.trace("assert that [{}] is greater than [{}]", actualValue, expectedValue); + assertThat(actualValue, instanceOf(Comparable.class)); + assertThat(expectedValue, instanceOf(Comparable.class)); + assertThat(errorMessage(), (Comparable)actualValue, greaterThan((Comparable) expectedValue)); + } + + private String errorMessage() { + return "field [" + getField() + "] is not greater than [" + getExpectedValue() + "]"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/IsFalseAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/IsFalseAssertion.java new file mode 100644 index 000000000000..d563d38e4248 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/IsFalseAssertion.java @@ -0,0 +1,61 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * Represents an is_false assert section: + * + * - is_false: get.fields.bar + * + */ +public class IsFalseAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(IsFalseAssertion.class); + + public IsFalseAssertion(String field) { + super(field, false); + } + + @Override + @SuppressWarnings("unchecked") + protected void doAssert(Object actualValue, Object expectedValue) { + logger.trace("assert that [{}] doesn't have a true value", actualValue); + + if (actualValue == null) { + return; + } + + String actualString = actualValue.toString(); + assertThat(errorMessage(), actualString, anyOf( + equalTo(""), + equalToIgnoringCase(Boolean.FALSE.toString()), + equalTo("0") + )); + } + + private String errorMessage() { + return "field [" + getField() + "] has a true value but it shouldn't"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/IsTrueAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/IsTrueAssertion.java new file mode 100644 index 000000000000..176b5ef98ffd --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/IsTrueAssertion.java @@ -0,0 +1,55 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * Represents an is_true assert section: + * + * - is_true: get.fields.bar + * + */ +public class IsTrueAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(IsTrueAssertion.class); + + public IsTrueAssertion(String field) { + super(field, true); + } + + @Override + protected void doAssert(Object actualValue, Object expectedValue) { + logger.trace("assert that [{}] has a true value", actualValue); + String errorMessage = errorMessage(); + assertThat(errorMessage, actualValue, notNullValue()); + String actualString = actualValue.toString(); + assertThat(errorMessage, actualString, not(equalTo(""))); + assertThat(errorMessage, actualString, not(equalToIgnoringCase(Boolean.FALSE.toString()))); + assertThat(errorMessage, actualString, not(equalTo("0"))); + } + + private String errorMessage() { + return "field [" + getField() + "] doesn't have a true value"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/LengthAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/LengthAssertion.java new file mode 100644 index 000000000000..8070634325d5 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/LengthAssertion.java @@ -0,0 +1,64 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; + +/** + * Represents a length assert section: + * + * - length: { hits.hits: 1 } + * + */ +public class LengthAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(LengthAssertion.class); + + public LengthAssertion(String field, Object expectedValue) { + super(field, expectedValue); + } + + @Override + protected void doAssert(Object actualValue, Object expectedValue) { + logger.trace("assert that [{}] has length [{}]", actualValue, expectedValue); + assertThat(expectedValue, instanceOf(Number.class)); + int length = ((Number) expectedValue).intValue(); + if (actualValue instanceof String) { + assertThat(errorMessage(), ((String) actualValue).length(), equalTo(length)); + } else if (actualValue instanceof List) { + assertThat(errorMessage(), ((List) actualValue).size(), equalTo(length)); + } else if (actualValue instanceof Map) { + assertThat(errorMessage(), ((Map) actualValue).keySet().size(), equalTo(length)); + } else { + throw new UnsupportedOperationException("value is of unsupported type [" + actualValue.getClass().getSimpleName() + "]"); + } + } + + private String errorMessage() { + return "field [" + getField() + "] doesn't have length [" + getExpectedValue() + "]"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/LessThanAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/LessThanAssertion.java new file mode 100644 index 000000000000..b5e609031207 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/LessThanAssertion.java @@ -0,0 +1,54 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertThat; + +/** + * Represents a lt assert section: + * + * - lt: { fields._ttl: 20000} + * + */ +public class LessThanAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(LessThanAssertion.class); + + public LessThanAssertion(String field, Object expectedValue) { + super(field, expectedValue); + } + + @Override + @SuppressWarnings("unchecked") + protected void doAssert(Object actualValue, Object expectedValue) { + logger.trace("assert that [{}] is less than [{}]", actualValue, expectedValue); + assertThat(actualValue, instanceOf(Comparable.class)); + assertThat(expectedValue, instanceOf(Comparable.class)); + assertThat(errorMessage(), (Comparable)actualValue, lessThan((Comparable)expectedValue)); + } + + private String errorMessage() { + return "field [" + getField() + "] is not less than [" + getExpectedValue() + "]"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/MatchAssertion.java b/src/test/java/org/elasticsearch/test/rest/section/MatchAssertion.java new file mode 100644 index 000000000000..c4823466f3d8 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/MatchAssertion.java @@ -0,0 +1,59 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.logging.Loggers; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +/** + * Represents a match assert section: + * + * - match: { get.fields._routing: "5" } + * + */ +public class MatchAssertion extends Assertion { + + private static final ESLogger logger = Loggers.getLogger(MatchAssertion.class); + + public MatchAssertion(String field, Object expectedValue) { + super(field, expectedValue); + } + + @Override + protected void doAssert(Object actualValue, Object expectedValue) { + assertThat(errorMessage(), actualValue, notNullValue()); + logger.trace("assert that [{}] matches [{}]", actualValue, expectedValue); + if (!actualValue.getClass().equals(expectedValue.getClass())) { + if (actualValue instanceof Number && expectedValue instanceof Number) { + //Double 1.0 is equals to Integer 1 + assertThat(errorMessage(), ((Number) actualValue).doubleValue(), equalTo(((Number) expectedValue).doubleValue())); + } + } else { + assertThat(errorMessage(), actualValue, equalTo(expectedValue)); + } + } + + private String errorMessage() { + return "field [" + getField() + "] doesn't match the expected value"; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/RestTestSuite.java b/src/test/java/org/elasticsearch/test/rest/section/RestTestSuite.java new file mode 100644 index 000000000000..ea19cb738955 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/RestTestSuite.java @@ -0,0 +1,78 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import com.google.common.collect.Lists; + +import java.io.File; +import java.util.List; + +/** + * Holds a REST test suite loaded from a specific yaml file. + * Supports a setup section and multiple test sections. + */ +public class RestTestSuite { + + private final String api; + private final String name; + + private SetupSection setupSection; + + private List testSections = Lists.newArrayList(); + + public RestTestSuite(String api, String name) { + this.api = replaceDot(api); + this.name = replaceDot(name); + } + + public String getApi() { + return api; + } + + public String getName() { + return name; + } + + //describes the rest test suite (e.g. index/10_with_id) + //useful also to reproduce failures (RestReproduceInfoPrinter) + public String getDescription() { + return api + File.separator + name; + } + + private static String replaceDot(String value) { + // '.' is used as separator internally and not expected to be within suite or test names, better replace it + return value.replace('.', '_'); + } + + public SetupSection getSetupSection() { + return setupSection; + } + + public void setSetupSection(SetupSection setupSection) { + this.setupSection = setupSection; + } + + public void addTestSection(TestSection testSection) { + this.testSections.add(testSection); + } + + public List getTestSections() { + return testSections; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/SetSection.java b/src/test/java/org/elasticsearch/test/rest/section/SetSection.java new file mode 100644 index 000000000000..08db06006d7b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/SetSection.java @@ -0,0 +1,52 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import com.google.common.collect.Maps; +import org.elasticsearch.test.rest.RestTestExecutionContext; + +import java.io.IOException; +import java.util.Map; + +/** + * Represents a set section: + * + * - set: {_scroll_id: scroll_id} + * + */ +public class SetSection implements ExecutableSection { + + private Map stash = Maps.newHashMap(); + + public void addSet(String responseField, String stashedField) { + stash.put(responseField, stashedField); + } + + public Map getStash() { + return stash; + } + + @Override + public void execute(RestTestExecutionContext executionContext) throws IOException { + for (Map.Entry entry : stash.entrySet()) { + Object actualValue = executionContext.response(entry.getKey()); + executionContext.stash(entry.getValue(), actualValue); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/SetupSection.java b/src/test/java/org/elasticsearch/test/rest/section/SetupSection.java new file mode 100644 index 000000000000..a8edfcc51692 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/SetupSection.java @@ -0,0 +1,60 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Represents a setup section. Holds a skip section and multiple do sections. + */ +public class SetupSection { + + public static final SetupSection EMPTY; + + static { + EMPTY = new SetupSection(); + EMPTY.setSkipSection(SkipSection.EMPTY); + } + + private SkipSection skipSection; + + private List doSections = Lists.newArrayList(); + + public SkipSection getSkipSection() { + return skipSection; + } + + public void setSkipSection(SkipSection skipSection) { + this.skipSection = skipSection; + } + + public List getDoSections() { + return doSections; + } + + public void addDoSection(DoSection doSection) { + this.doSections.add(doSection); + } + + public boolean isEmpty() { + return EMPTY.equals(this); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/SkipSection.java b/src/test/java/org/elasticsearch/test/rest/section/SkipSection.java new file mode 100644 index 000000000000..bfd713f84ced --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/SkipSection.java @@ -0,0 +1,57 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import org.elasticsearch.test.rest.support.VersionUtils; + +/** + * Represents a skip section that tells whether a specific test section or suite needs to be skipped + * based on the elasticsearch version the tests are running against. + */ +public class SkipSection { + + public static final SkipSection EMPTY = new SkipSection("", ""); + + private final String version; + private final String reason; + + public SkipSection(String version, String reason) { + this.version = version; + this.reason = reason; + } + + public String getVersion() { + return version; + } + + public String getReason() { + return reason; + } + + public boolean skipVersion(String currentVersion) { + if (isEmpty()) { + return false; + } + return VersionUtils.skipCurrentVersion(version, currentVersion); + } + + public boolean isEmpty() { + return EMPTY.equals(this); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/section/TestSection.java b/src/test/java/org/elasticsearch/test/rest/section/TestSection.java new file mode 100644 index 000000000000..0b32ba570502 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/section/TestSection.java @@ -0,0 +1,57 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.section; + +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * Represents a test section, which is composed of a skip section and multiple executable sections. + */ +public class TestSection { + private final String name; + private SkipSection skipSection; + private final List executableSections; + + public TestSection(String name) { + this.name = name; + this.executableSections = Lists.newArrayList(); + } + + public String getName() { + return name; + } + + public SkipSection getSkipSection() { + return skipSection; + } + + public void setSkipSection(SkipSection skipSection) { + this.skipSection = skipSection; + } + + public List getExecutableSections() { + return executableSections; + } + + public void addExecutableSection(ExecutableSection executableSection) { + this.executableSections.add(executableSection); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/spec/RestApi.java b/src/test/java/org/elasticsearch/test/rest/spec/RestApi.java new file mode 100644 index 000000000000..478484bf6282 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/spec/RestApi.java @@ -0,0 +1,214 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.spec; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.lucene.util.PriorityQueue; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents an elasticsearch REST endpoint (api) + */ +public class RestApi { + + private static final String ALL = "_all"; + + private final String name; + private List methods = Lists.newArrayList(); + private List paths = Lists.newArrayList(); + private List pathParts = Lists.newArrayList(); + + RestApi(String name) { + this.name = name; + } + + RestApi(RestApi restApi, String name, String... methods) { + this.name = name; + this.methods = Arrays.asList(methods); + paths.addAll(restApi.getPaths()); + pathParts.addAll(restApi.getPathParts()); + } + + RestApi(RestApi restApi, List paths) { + this.name = restApi.getName(); + this.methods = restApi.getMethods(); + this.paths.addAll(paths); + pathParts.addAll(restApi.getPathParts()); + } + + public String getName() { + return name; + } + + public List getMethods() { + return methods; + } + + /** + * Returns the supported http methods given the rest parameters provided + */ + public List getSupportedMethods(Set restParams) { + //we try to avoid hardcoded mappings but the index api is the exception + if ("index".equals(name) || "create".equals(name)) { + List indexMethods = Lists.newArrayList(); + for (String method : methods) { + if (restParams.contains("id")) { + //PUT when the id is provided + if (HttpPut.METHOD_NAME.equals(method)) { + indexMethods.add(method); + } + } else { + //POST without id + if (HttpPost.METHOD_NAME.equals(method)) { + indexMethods.add(method); + } + } + } + return indexMethods; + } + + return methods; + } + + void addMethod(String method) { + this.methods.add(method); + } + + public List getPaths() { + return paths; + } + + void addPath(String path) { + this.paths.add(path); + } + + public List getPathParts() { + return pathParts; + } + + void addPathPart(String pathPart) { + this.pathParts.add(pathPart); + } + + /** + * Finds the best matching rest path given the current parameters and replaces + * placeholders with their corresponding values received as arguments + */ + public String getFinalPath(Map pathParams) { + RestPath matchingRestPath = findMatchingRestPath(pathParams.keySet()); + String path = matchingRestPath.path; + for (Map.Entry paramEntry : matchingRestPath.params.entrySet()) { + //replace path placeholders with actual values + String value = pathParams.get(paramEntry.getValue()); + if (value == null) { + //there might be additional placeholder to replace, not available as input params + //it can only be {index} or {type} to be replaced with _all + if (paramEntry.getValue().equals("index") || paramEntry.getValue().equals("type")) { + value = ALL; + } else { + throw new IllegalArgumentException("path [" + path + "] contains placeholders that weren't replaced with proper values"); + } + } + path = path.replace(paramEntry.getKey(), value); + } + return path; + } + + /** + * Finds the best matching rest path out of the available ones with the current api (based on REST spec). + * + * The best path is the one that has exactly the same number of placeholders to replace + * (e.g. /{index}/{type}/{id} when the params are exactly index, type and id). + * Otherwise there might be additional placeholders, thus we use the path with the least additional placeholders. + * (e.g. get with only index and id as parameters, the closest (and only) path contains {type} too, which becomes _all) + */ + private RestPath findMatchingRestPath(Set restParams) { + + RestPath[] restPaths = buildRestPaths(); + + //We need to find the path that has exactly the placeholders corresponding to our params + //If there's no exact match we fallback to the closest one (with as less additional placeholders as possible) + //The fallback is needed for: + //1) get, get_source and exists with only index and id => /{index}/_all/{id} ( + //2) search with only type => /_all/{type/_search + PriorityQueue restPathQueue = new PriorityQueue(1) { + @Override + protected boolean lessThan(RestPath a, RestPath b) { + return a.params.size() >= b.params.size(); + } + }; + for (RestPath restPath : restPaths) { + if (restPath.params.values().containsAll(restParams)) { + restPathQueue.insertWithOverflow(restPath); + } + } + + if (restPathQueue.size() > 0) { + return restPathQueue.top(); + } + + throw new IllegalArgumentException("unable to find best path for api [" + name + "] and params " + restParams); + } + + private RestPath[] buildRestPaths() { + RestPath[] restPaths = new RestPath[paths.size()]; + for (int i = 0; i < restPaths.length; i++) { + restPaths[i] = new RestPath(paths.get(i)); + } + return restPaths; + } + + private static class RestPath { + private static final Pattern PLACEHOLDERS_PATTERN = Pattern.compile("(\\{(.*?)})"); + + final String path; + //contains param to replace (e.g. {index}) and param key to use for lookup in the current values map (e.g. index) + final Map params; + + RestPath(String path) { + this.path = path; + this.params = extractParams(path); + } + + private static Map extractParams(String input) { + Map params = Maps.newHashMap(); + Matcher matcher = PLACEHOLDERS_PATTERN.matcher(input); + while (matcher.find()) { + //key is e.g. {index} + String key = input.substring(matcher.start(), matcher.end()); + if (matcher.groupCount() != 2) { + throw new IllegalArgumentException("no lookup key found for param [" + key + "]"); + } + //to be replaced with current value found with key e.g. index + String value = matcher.group(2); + params.put(key, value); + } + return params; + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/spec/RestApiParser.java b/src/test/java/org/elasticsearch/test/rest/spec/RestApiParser.java new file mode 100644 index 000000000000..114615d7664b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/spec/RestApiParser.java @@ -0,0 +1,103 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.spec; + +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * Parser for a REST api spec (single json file) + */ +public class RestApiParser { + + public RestApi parse(XContentParser parser) throws IOException { + + try { + while ( parser.nextToken() != XContentParser.Token.FIELD_NAME ) { + //move to first field name + } + + RestApi restApi = new RestApi(parser.currentName()); + + int level = -1; + while (parser.nextToken() != XContentParser.Token.END_OBJECT || level >= 0) { + + if (parser.currentToken() == XContentParser.Token.FIELD_NAME) { + if ("methods".equals(parser.currentName())) { + parser.nextToken(); + while (parser.nextToken() == XContentParser.Token.VALUE_STRING) { + restApi.addMethod(parser.text()); + } + } + + if ("url".equals(parser.currentName())) { + String currentFieldName = "url"; + int innerLevel = -1; + while(parser.nextToken() != XContentParser.Token.END_OBJECT || innerLevel >= 0) { + if (parser.currentToken() == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } + + if (parser.currentToken() == XContentParser.Token.START_ARRAY && "paths".equals(currentFieldName)) { + while (parser.nextToken() == XContentParser.Token.VALUE_STRING) { + restApi.addPath(parser.text()); + } + } + + if (parser.currentToken() == XContentParser.Token.START_OBJECT && "parts".equals(currentFieldName)) { + while (parser.nextToken() == XContentParser.Token.FIELD_NAME) { + restApi.addPathPart(parser.currentName()); + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + parser.skipChildren(); + } + } + + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + innerLevel++; + } + if (parser.currentToken() == XContentParser.Token.END_OBJECT) { + innerLevel--; + } + } + } + } + + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + level++; + } + if (parser.currentToken() == XContentParser.Token.END_OBJECT) { + level--; + } + + } + + parser.nextToken(); + assert parser.currentToken() == XContentParser.Token.END_OBJECT; + parser.nextToken(); + + return restApi; + + } finally { + parser.close(); + } + } + +} diff --git a/src/test/java/org/elasticsearch/test/rest/spec/RestSpec.java b/src/test/java/org/elasticsearch/test/rest/spec/RestSpec.java new file mode 100644 index 000000000000..f697b13e0d1b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/spec/RestSpec.java @@ -0,0 +1,81 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.spec; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.rest.support.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Holds the elasticsearch REST spec + */ +public class RestSpec { + Map restApiMap = Maps.newHashMap(); + + private RestSpec() { + } + + void addApi(RestApi restApi) { + if ("info".equals(restApi.getName())) { + //info and ping should really be two different api in the rest spec + //info (GET|HEAD /) needs to be manually split into 1) info: GET / 2) ping: HEAD / + restApiMap.put("info", new RestApi(restApi, "info", "GET")); + restApiMap.put("ping", new RestApi(restApi, "ping", "HEAD")); + } else if ("get".equals(restApi.getName())) { + //get_source endpoint shouldn't be present in the rest spec for the get api + //as get_source is already a separate api + List paths = Lists.newArrayList(); + for (String path : restApi.getPaths()) { + if (!path.endsWith("/_source")) { + paths.add(path); + } + } + restApiMap.put(restApi.getName(), new RestApi(restApi, paths)); + } else { + restApiMap.put(restApi.getName(), restApi); + } + } + + public RestApi getApi(String api) { + return restApiMap.get(api); + } + + /** + * Parses the complete set of REST spec available under the provided directories + */ + public static RestSpec parseFrom(String optionalPathPrefix, String... paths) throws IOException { + RestSpec restSpec = new RestSpec(); + for (String path : paths) { + for (File jsonFile : FileUtils.findJsonSpec(optionalPathPrefix, path)) { + XContentParser parser = JsonXContent.jsonXContent.createParser(new FileInputStream(jsonFile)); + RestApi restApi = new RestApiParser().parse(parser); + restSpec.addApi(restApi); + } + } + return restSpec; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/support/FileUtils.java b/src/test/java/org/elasticsearch/test/rest/support/FileUtils.java new file mode 100644 index 000000000000..3b93f4affca7 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/support/FileUtils.java @@ -0,0 +1,144 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.support; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.elasticsearch.common.Strings; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.net.URL; +import java.util.Map; +import java.util.Set; + +public final class FileUtils { + + private static final String YAML_SUFFIX = ".yaml"; + private static final String JSON_SUFFIX = ".json"; + + private FileUtils() { + + } + + /** + * Returns the json files found within the directory provided as argument. + * Files are looked up in the classpath first, then outside of it if not found. + */ + public static Set findJsonSpec(String optionalPathPrefix, String path) throws FileNotFoundException { + File dir = resolveFile(optionalPathPrefix, path, null); + + if (!dir.isDirectory()) { + throw new FileNotFoundException("file [" + path + "] is not a directory"); + } + + File[] jsonFiles = dir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.getName().endsWith(JSON_SUFFIX); + } + }); + + if (jsonFiles == null || jsonFiles.length == 0) { + throw new FileNotFoundException("no json files found within [" + path + "]"); + } + + return Sets.newHashSet(jsonFiles); + } + + /** + * Returns the yaml files found within the paths provided. + * Each input path can either be a single file (the .yaml suffix is optional) or a directory. + * Each path is looked up in the classpath first, then outside of it if not found yet. + */ + public static Map> findYamlSuites(final String optionalPathPrefix, final String... paths) throws FileNotFoundException { + Map> yamlSuites = Maps.newHashMap(); + for (String path : paths) { + collectFiles(resolveFile(optionalPathPrefix, path, YAML_SUFFIX), YAML_SUFFIX, yamlSuites); + } + return yamlSuites; + } + + private static File resolveFile(String optionalPathPrefix, String path, String optionalFileSuffix) throws FileNotFoundException { + //try within classpath with and without file suffix (as it could be a single test suite) + URL resource = findResource(path, optionalFileSuffix); + if (resource == null) { + //try within classpath with optional prefix: /rest-spec/test (or /rest-test/api) is optional + String newPath = optionalPathPrefix + File.separator + path; + resource = findResource(newPath, optionalFileSuffix); + if (resource == null) { + //if it wasn't on classpath we look outside ouf the classpath + File file = findFile(path, optionalFileSuffix); + if (!file.exists()) { + throw new FileNotFoundException("file [" + path + "] doesn't exist"); + } + return file; + } + } + return new File(resource.getFile()); + } + + private static URL findResource(String path, String optionalFileSuffix) { + URL resource = FileUtils.class.getResource(path); + if (resource == null) { + //if not found we append the file suffix to the path (as it is optional) + if (Strings.hasLength(optionalFileSuffix) && !path.endsWith(optionalFileSuffix)) { + resource = FileUtils.class.getResource(path + optionalFileSuffix); + } + } + return resource; + } + + private static File findFile(String path, String optionalFileSuffix) { + File file = new File(path); + if (!file.exists()) { + file = new File(path + optionalFileSuffix); + } + return file; + } + + private static void collectFiles(final File file, final String fileSuffix, final Map> files) { + if (file.isFile()) { + // '.' is uses as separator internally and not expected to be within suite or test names, better replace it + String groupName = file.getParentFile().getName().replace('.', '_'); + Set filesSet = files.get(groupName); + if (filesSet == null) { + filesSet = Sets.newHashSet(); + files.put(groupName, filesSet); + } + filesSet.add(file); + } else if (file.isDirectory()) { + walkDir(file, fileSuffix, files); + } + } + + private static void walkDir(final File dir, final String fileSuffix, final Map> files) { + File[] children = dir.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory() || pathname.getName().endsWith(fileSuffix); + } + }); + + for (File file : children) { + collectFiles(file, fileSuffix, files); + } + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/support/VersionUtils.java b/src/test/java/org/elasticsearch/test/rest/support/VersionUtils.java new file mode 100644 index 000000000000..f3290f8ab75a --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/support/VersionUtils.java @@ -0,0 +1,87 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.support; + +public final class VersionUtils { + + private VersionUtils() { + + } + + /** + * Parses an elasticsearch version string into an int array with an element per part + * e.g. 0.90.7 => [0,90,7] + */ + public static int[] parseVersionNumber(String version) { + String[] split = version.split("\\."); + //we only take the first 3 parts if there are more, but less is ok too (e.g. 999) + int length = Math.min(3, split.length); + int[] versionNumber = new int[length]; + for (int i = 0; i < length; i++) { + try { + versionNumber[i] = Integer.valueOf(split[i]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("version is not a number", e); + } + + } + return versionNumber; + } + + /** + * Compares the skip version read from a test fragment with the elasticsearch version + * the tests are running against and determines whether the test fragment needs to be skipped + */ + public static boolean skipCurrentVersion(String skipVersion, String currentVersion) { + int[] currentVersionNumber = parseVersionNumber(currentVersion); + + String[] skipVersions = skipVersion.split("-"); + if (skipVersions.length > 2) { + throw new IllegalArgumentException("too many skip versions found"); + } + + String skipVersionLowerBound = skipVersions[0].trim(); + String skipVersionUpperBound = skipVersions[1].trim(); + + int[] skipVersionLowerBoundNumber = parseVersionNumber(skipVersionLowerBound); + int[] skipVersionUpperBoundNumber = parseVersionNumber(skipVersionUpperBound); + + int length = Math.min(skipVersionLowerBoundNumber.length, currentVersionNumber.length); + for (int i = 0; i < length; i++) { + if (currentVersionNumber[i] < skipVersionLowerBoundNumber[i]) { + return false; + } + if (currentVersionNumber[i] > skipVersionLowerBoundNumber[i]) { + break; + } + } + + length = Math.min(skipVersionUpperBoundNumber.length, currentVersionNumber.length); + for (int i = 0; i < length; i++) { + if (currentVersionNumber[i] > skipVersionUpperBoundNumber[i]) { + return false; + } + if (currentVersionNumber[i] < skipVersionUpperBoundNumber[i]) { + break; + } + } + + return true; + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/AbstractParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/AbstractParserTests.java new file mode 100644 index 000000000000..78e1fb9a9825 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/AbstractParserTests.java @@ -0,0 +1,41 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.After; +import org.junit.Ignore; + +import static org.hamcrest.Matchers.nullValue; + +@Ignore +public class AbstractParserTests extends ElasticsearchTestCase { + + protected XContentParser parser; + + @After + public void tearDown() throws Exception { + super.tearDown(); + //this is the way to make sure that we consumed the whole yaml + assertThat(parser.currentToken(), nullValue()); + parser.close(); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/AssertionParsersTests.java b/src/test/java/org/elasticsearch/test/rest/test/AssertionParsersTests.java new file mode 100644 index 000000000000..df9d9ef15f4b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/AssertionParsersTests.java @@ -0,0 +1,174 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.*; +import org.elasticsearch.test.rest.section.*; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.*; + +public class AssertionParsersTests extends AbstractParserTests { + + @Test + public void testParseIsTrue() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "get.fields._timestamp" + ); + + IsTrueParser isTrueParser = new IsTrueParser(); + IsTrueAssertion trueAssertion = isTrueParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(trueAssertion, notNullValue()); + assertThat(trueAssertion.getField(), equalTo("get.fields._timestamp")); + } + + @Test + public void testParseIsFalse() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "docs.1._source" + ); + + IsFalseParser isFalseParser = new IsFalseParser(); + IsFalseAssertion falseAssertion = isFalseParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(falseAssertion, notNullValue()); + assertThat(falseAssertion.getField(), equalTo("docs.1._source")); + } + + @Test + public void testParseGreaterThan() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ field: 3}" + ); + + GreaterThanParser greaterThanParser = new GreaterThanParser(); + GreaterThanAssertion greaterThanAssertion = greaterThanParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + assertThat(greaterThanAssertion, notNullValue()); + assertThat(greaterThanAssertion.getField(), equalTo("field")); + assertThat(greaterThanAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) greaterThanAssertion.getExpectedValue(), equalTo(3)); + } + + @Test + public void testParseLessThan() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ field: 3}" + ); + + LessThanParser lessThanParser = new LessThanParser(); + LessThanAssertion lessThanAssertion = lessThanParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + assertThat(lessThanAssertion, notNullValue()); + assertThat(lessThanAssertion.getField(), equalTo("field")); + assertThat(lessThanAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) lessThanAssertion.getExpectedValue(), equalTo(3)); + } + + @Test + public void testParseLength() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ _id: 22}" + ); + + LengthParser lengthParser = new LengthParser(); + LengthAssertion lengthAssertion = lengthParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + assertThat(lengthAssertion, notNullValue()); + assertThat(lengthAssertion.getField(), equalTo("_id")); + assertThat(lengthAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) lengthAssertion.getExpectedValue(), equalTo(22)); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseMatchSimpleIntegerValue() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ field: 10 }" + ); + + MatchParser matchParser = new MatchParser(); + MatchAssertion matchAssertion = matchParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(matchAssertion, notNullValue()); + assertThat(matchAssertion.getField(), equalTo("field")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) matchAssertion.getExpectedValue(), equalTo(10)); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseMatchSimpleStringValue() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ foo: bar }" + ); + + MatchParser matchParser = new MatchParser(); + MatchAssertion matchAssertion = matchParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(matchAssertion, notNullValue()); + assertThat(matchAssertion.getField(), equalTo("foo")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(String.class)); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("bar")); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseMatchArray() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{'matches': ['test_percolator_1', 'test_percolator_2']}" + ); + + MatchParser matchParser = new MatchParser(); + MatchAssertion matchAssertion = matchParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(matchAssertion, notNullValue()); + assertThat(matchAssertion.getField(), equalTo("matches")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(List.class)); + List strings = (List) matchAssertion.getExpectedValue(); + assertThat(strings.size(), equalTo(2)); + assertThat(strings.get(0).toString(), equalTo("test_percolator_1")); + assertThat(strings.get(1).toString(), equalTo("test_percolator_2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testParseMatchSourceValues() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ _source: { responses.0.hits.total: 3, foo: bar }}" + ); + + MatchParser matchParser = new MatchParser(); + MatchAssertion matchAssertion = matchParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(matchAssertion, notNullValue()); + assertThat(matchAssertion.getField(), equalTo("_source")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(Map.class)); + Map expectedValue = (Map) matchAssertion.getExpectedValue(); + assertThat(expectedValue.size(), equalTo(2)); + Object o = expectedValue.get("responses.0.hits.total"); + assertThat(o, instanceOf(Integer.class)); + assertThat((Integer)o, equalTo(3)); + o = expectedValue.get("foo"); + assertThat(o, instanceOf(String.class)); + assertThat(o.toString(), equalTo("bar")); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/DoSectionParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/DoSectionParserTests.java new file mode 100644 index 000000000000..7b6958bccb26 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/DoSectionParserTests.java @@ -0,0 +1,420 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.DoSectionParser; +import org.elasticsearch.test.rest.parser.RestTestParseException; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.section.ApiCallSection; +import org.elasticsearch.test.rest.section.DoSection; +import org.hamcrest.MatcherAssert; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class DoSectionParserTests extends AbstractParserTests { + + @Test + public void testParseDoSectionNoBody() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "get:\n" + + " index: test_index\n" + + " type: test_type\n" + + " id: 1" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("get")); + assertThat(apiCallSection.getParams().size(), equalTo(3)); + assertThat(apiCallSection.getParams().get("index"), equalTo("test_index")); + assertThat(apiCallSection.getParams().get("type"), equalTo("test_type")); + assertThat(apiCallSection.getParams().get("id"), equalTo("1")); + assertThat(apiCallSection.hasBody(), equalTo(false)); + } + + @Test + public void testParseDoSectionNoParamsNoBody() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "cluster.node_info: {}" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("cluster.node_info")); + assertThat(apiCallSection.getParams().size(), equalTo(0)); + assertThat(apiCallSection.hasBody(), equalTo(false)); + } + + @Test + public void testParseDoSectionWithJsonBody() throws Exception { + String body = "{ \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }"; + parser = YamlXContent.yamlXContent.createParser( + "index:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: " + body + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("index")); + assertThat(apiCallSection.getParams().size(), equalTo(3)); + assertThat(apiCallSection.getParams().get("index"), equalTo("test_1")); + assertThat(apiCallSection.getParams().get("type"), equalTo("test")); + assertThat(apiCallSection.getParams().get("id"), equalTo("1")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + + assertJsonEquals(apiCallSection.getBodiesAsList().get(0), body); + assertJsonEquals(apiCallSection.getBody(), body); + } + + @Test + public void testParseDoSectionWithJsonMultipleBodiesAsLongString() throws Exception { + String bodies[] = new String[]{ + "{ \"index\": { \"_index\":\"test_index\", \"_type\":\"test_type\", \"_id\":\"test_id\" } }\n", + "{ \"f1\":\"v1\", \"f2\":42 }\n", + "{ \"index\": { \"_index\":\"test_index2\", \"_type\":\"test_type2\", \"_id\":\"test_id2\" } }\n", + "{ \"f1\":\"v2\", \"f2\":47 }\n" + }; + parser = YamlXContent.yamlXContent.createParser( + "bulk:\n" + + " refresh: true\n" + + " body: |\n" + + " " + bodies[0] + + " " + bodies[1] + + " " + bodies[2] + + " " + bodies[3] + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("bulk")); + assertThat(apiCallSection.getParams().size(), equalTo(1)); + assertThat(apiCallSection.getParams().get("refresh"), equalTo("true")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(1)); + StringBuilder bodyBuilder = new StringBuilder(); + for (String body : bodies) { + bodyBuilder.append(body); + } + assertThat(apiCallSection.getBody(), equalTo(bodyBuilder.toString())); + } + + @Test + public void testParseDoSectionWithJsonMultipleBodiesRepeatedProperty() throws Exception { + String[] bodies = new String[] { + "{ \"index\": { \"_index\":\"test_index\", \"_type\":\"test_type\", \"_id\":\"test_id\" } }", + "{ \"f1\":\"v1\", \"f2\":42 }", + }; + parser = YamlXContent.yamlXContent.createParser( + "bulk:\n" + + " refresh: true\n" + + " body: \n" + + " " + bodies[0] + "\n" + + " body: \n" + + " " + bodies[1] + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("bulk")); + assertThat(apiCallSection.getParams().size(), equalTo(1)); + assertThat(apiCallSection.getParams().get("refresh"), equalTo("true")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(bodies.length)); + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(apiCallSection.getBodiesAsList().get(i), bodies[i]); + } + + String[] returnedBodies = apiCallSection.getBody().split("\n"); + assertThat(returnedBodies.length, equalTo(bodies.length)); + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(returnedBodies[i], bodies[i]); + } + } + + @Test + public void testParseDoSectionWithYamlBody() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "search:\n" + + " body:\n" + + " _source: [ include.field1, include.field2 ]\n" + + " query: { match_all: {} }" + ); + String body = "{ \"_source\": [ \"include.field1\", \"include.field2\" ], \"query\": { \"match_all\": {} }}"; + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("search")); + assertThat(apiCallSection.getParams().size(), equalTo(0)); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(1)); + assertJsonEquals(apiCallSection.getBodiesAsList().get(0), body); + assertJsonEquals(apiCallSection.getBody(), body); + } + + @Test + public void testParseDoSectionWithYamlMultipleBodies() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "bulk:\n" + + " refresh: true\n" + + " body:\n" + + " - index:\n" + + " _index: test_index\n" + + " _type: test_type\n" + + " _id: test_id\n" + + " - f1: v1\n" + + " f2: 42\n" + + " - index:\n" + + " _index: test_index2\n" + + " _type: test_type2\n" + + " _id: test_id2\n" + + " - f1: v2\n" + + " f2: 47" + ); + String[] bodies = new String[4]; + bodies[0] = "{\"index\": {\"_index\": \"test_index\", \"_type\": \"test_type\", \"_id\": \"test_id\"}}"; + bodies[1] = "{ \"f1\":\"v1\", \"f2\": 42 }"; + bodies[2] = "{\"index\": {\"_index\": \"test_index2\", \"_type\": \"test_type2\", \"_id\": \"test_id2\"}}"; + bodies[3] = "{ \"f1\":\"v2\", \"f2\": 47 }"; + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("bulk")); + assertThat(apiCallSection.getParams().size(), equalTo(1)); + assertThat(apiCallSection.getParams().get("refresh"), equalTo("true")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(bodies.length)); + + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(apiCallSection.getBodiesAsList().get(i), bodies[i]); + } + + String[] returnedBodies = apiCallSection.getBody().split("\n"); + assertThat(returnedBodies.length, equalTo(bodies.length)); + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(returnedBodies[i], bodies[i]); + } + } + + @Test + public void testParseDoSectionWithYamlMultipleBodiesRepeatedProperty() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "bulk:\n" + + " refresh: true\n" + + " body:\n" + + " index:\n" + + " _index: test_index\n" + + " _type: test_type\n" + + " _id: test_id\n" + + " body:\n" + + " f1: v1\n" + + " f2: 42\n" + ); + String[] bodies = new String[2]; + bodies[0] = "{\"index\": {\"_index\": \"test_index\", \"_type\": \"test_type\", \"_id\": \"test_id\"}}"; + bodies[1] = "{ \"f1\":\"v1\", \"f2\": 42 }"; + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("bulk")); + assertThat(apiCallSection.getParams().size(), equalTo(1)); + assertThat(apiCallSection.getParams().get("refresh"), equalTo("true")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(bodies.length)); + + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(apiCallSection.getBodiesAsList().get(i), bodies[i]); + } + + String[] returnedBodies = apiCallSection.getBody().split("\n"); + assertThat(returnedBodies.length, equalTo(bodies.length)); + for (int i = 0; i < bodies.length; i++) { + assertJsonEquals(returnedBodies[i], bodies[i]); + } + } + + @Test + public void testParseDoSectionWithYamlBodyMultiGet() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "mget:\n" + + " body:\n" + + " docs:\n" + + " - { _index: test_2, _type: test, _id: 1}\n" + + " - { _index: test_1, _type: none, _id: 1}" + ); + String body = "{ \"docs\": [ " + + "{\"_index\": \"test_2\", \"_type\":\"test\", \"_id\":1}, " + + "{\"_index\": \"test_1\", \"_type\":\"none\", \"_id\":1} " + + "]}"; + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("mget")); + assertThat(apiCallSection.getParams().size(), equalTo(0)); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(1)); + assertJsonEquals(apiCallSection.getBodiesAsList().get(0), body); + assertJsonEquals(apiCallSection.getBody(), body); + } + + @Test + public void testParseDoSectionWithBodyStringified() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "index:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: \"{ _source: true, query: { match_all: {} } }\"" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection, notNullValue()); + assertThat(apiCallSection.getApi(), equalTo("index")); + assertThat(apiCallSection.getParams().size(), equalTo(3)); + assertThat(apiCallSection.getParams().get("index"), equalTo("test_1")); + assertThat(apiCallSection.getParams().get("type"), equalTo("test")); + assertThat(apiCallSection.getParams().get("id"), equalTo("1")); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(1)); + //stringified body is taken as is + assertThat(apiCallSection.getBodiesAsList().get(0), equalTo("{ _source: true, query: { match_all: {} } }")); + } + + @Test + public void testParseDoSectionWithBodiesStringifiedAndNot() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "index:\n" + + " body:\n" + + " - \"{ _source: true, query: { match_all: {} } }\"\n" + + " - { size: 100, query: { match_all: {} } }" + ); + + String body = "{ \"size\": 100, \"query\": { \"match_all\": {} } }"; + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + ApiCallSection apiCallSection = doSection.getApiCallSection(); + + assertThat(apiCallSection.getApi(), equalTo("index")); + assertThat(apiCallSection.getParams().size(), equalTo(0)); + assertThat(apiCallSection.hasBody(), equalTo(true)); + assertThat(apiCallSection.getBodiesAsList().size(), equalTo(2)); + //stringified body is taken as is + assertThat(apiCallSection.getBodiesAsList().get(0), equalTo("{ _source: true, query: { match_all: {} } }")); + assertJsonEquals(apiCallSection.getBodiesAsList().get(1), body); + } + + @Test + public void testParseDoSectionWithCatch() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "catch: missing\n" + + "indices.get_warmer:\n" + + " index: test_index\n" + + " name: test_warmer" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(doSection.getCatch(), equalTo("missing")); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_warmer")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(2)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + } + + @Test (expected = RestTestParseException.class) + public void testParseDoSectionWithoutClientCallSection() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "catch: missing\n" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + } + + @Test + public void testParseDoSectionMultivaluedField() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "indices.get_field_mapping:\n" + + " index: test_index\n" + + " type: test_type\n" + + " field: [ text , text1 ]" + ); + + DoSectionParser doSectionParser = new DoSectionParser(); + DoSection doSection = doSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_field_mapping")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().getParams().get("index"), equalTo("test_index")); + assertThat(doSection.getApiCallSection().getParams().get("type"), equalTo("test_type")); + assertThat(doSection.getApiCallSection().getParams().get("field"), equalTo("text,text1")); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + assertThat(doSection.getApiCallSection().getBodiesAsList().size(), equalTo(0)); + } + + private static void assertJsonEquals(String actual, String expected) throws IOException { + Map actualMap = JsonXContent.jsonXContent.createParser(actual).mapOrderedAndClose(); + Map expectedMap = JsonXContent.jsonXContent.createParser(expected).mapOrderedAndClose(); + MatcherAssert.assertThat(actualMap, equalTo(expectedMap)); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/FileUtilsTests.java b/src/test/java/org/elasticsearch/test/rest/test/FileUtilsTests.java new file mode 100644 index 000000000000..1fdbbb84ba91 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/FileUtilsTests.java @@ -0,0 +1,116 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.test.rest.support.FileUtils; +import org.junit.Test; + +import java.io.File; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.Matchers.greaterThan; + +public class FileUtilsTests extends ElasticsearchTestCase { + + @Test + public void testLoadSingleYamlSuite() throws Exception { + Map> yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "/rest-spec/test/get/10_basic"); + assertSingleFile(yamlSuites, "get", "10_basic.yaml"); + + //the path prefix is optional + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get/10_basic.yaml"); + assertSingleFile(yamlSuites, "get", "10_basic.yaml"); + + //extension .yaml is optional + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get/10_basic"); + assertSingleFile(yamlSuites, "get", "10_basic.yaml"); + } + + @Test + public void testLoadMultipleYamlSuites() throws Exception { + //single directory + Map> yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get"); + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(1)); + assertThat(yamlSuites.containsKey("get"), equalTo(true)); + assertThat(yamlSuites.get("get").size(), greaterThan(1)); + + //multiple directories + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get", "index"); + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(2)); + assertThat(yamlSuites.containsKey("get"), equalTo(true)); + assertThat(yamlSuites.get("get").size(), greaterThan(1)); + assertThat(yamlSuites.containsKey("index"), equalTo(true)); + assertThat(yamlSuites.get("index").size(), greaterThan(1)); + + //multiple paths, which can be both directories or yaml test suites (with optional file extension) + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get/10_basic", "index"); + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(2)); + assertThat(yamlSuites.containsKey("get"), equalTo(true)); + assertThat(yamlSuites.get("get").size(), equalTo(1)); + assertSingleFile(yamlSuites.get("get"), "get", "10_basic.yaml"); + assertThat(yamlSuites.containsKey("index"), equalTo(true)); + assertThat(yamlSuites.get("index").size(), greaterThan(1)); + + //files can be loaded from classpath and from file system too + File dir = newTempDir(); + File file = new File(dir, "test_loading.yaml"); + assertThat(file.createNewFile(), equalTo(true)); + + //load from directory outside of the classpath + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get/10_basic", dir.getAbsolutePath()); + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(2)); + assertThat(yamlSuites.containsKey("get"), equalTo(true)); + assertThat(yamlSuites.get("get").size(), equalTo(1)); + assertSingleFile(yamlSuites.get("get"), "get", "10_basic.yaml"); + assertThat(yamlSuites.containsKey(dir.getName()), equalTo(true)); + assertSingleFile(yamlSuites.get(dir.getName()), dir.getName(), file.getName()); + + //load from external file (optional extension) + yamlSuites = FileUtils.findYamlSuites("/rest-spec/test", "get/10_basic", dir.getAbsolutePath() + File.separator + "test_loading"); + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(2)); + assertThat(yamlSuites.containsKey("get"), equalTo(true)); + assertThat(yamlSuites.get("get").size(), equalTo(1)); + assertSingleFile(yamlSuites.get("get"), "get", "10_basic.yaml"); + assertThat(yamlSuites.containsKey(dir.getName()), equalTo(true)); + assertSingleFile(yamlSuites.get(dir.getName()), dir.getName(), file.getName()); + } + + private static void assertSingleFile(Map> yamlSuites, String dirName, String fileName) { + assertThat(yamlSuites, notNullValue()); + assertThat(yamlSuites.size(), equalTo(1)); + assertThat(yamlSuites.containsKey(dirName), equalTo(true)); + assertSingleFile(yamlSuites.get(dirName), dirName, fileName); + } + + private static void assertSingleFile(Set files, String dirName, String fileName) { + assertThat(files.size(), equalTo(1)); + File file = files.iterator().next(); + assertThat(file.getName(), equalTo(fileName)); + assertThat(file.getParentFile().getName(), equalTo(dirName)); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/JsonPathTests.java b/src/test/java/org/elasticsearch/test/rest/test/JsonPathTests.java new file mode 100644 index 000000000000..3382ca5430a9 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/JsonPathTests.java @@ -0,0 +1,150 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.test.rest.json.JsonPath; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.*; + +public class JsonPathTests extends ElasticsearchTestCase { + + @Test + public void testEvaluateObjectPathEscape() throws Exception { + String json = "{ \"field1\": { \"field2.field3\" : \"value2\" } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.field2\\.field3"); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateObjectPathWithDoubleDot() throws Exception { + String json = "{ \"field1\": { \"field2\" : \"value2\" } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1..field2"); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateObjectPathEndsWithDot() throws Exception { + String json = "{ \"field1\": { \"field2\" : \"value2\" } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.field2."); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateString() throws Exception { + String json = "{ \"field1\": { \"field2\" : \"value2\" } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.field2"); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateInteger() throws Exception { + String json = "{ \"field1\": { \"field2\" : 333 } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.field2"); + assertThat(object, instanceOf(Integer.class)); + assertThat((Integer)object, equalTo(333)); + } + + @Test + public void testEvaluateDouble() throws Exception { + String json = "{ \"field1\": { \"field2\" : 3.55 } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.field2"); + assertThat(object, instanceOf(Double.class)); + assertThat((Double)object, equalTo(3.55)); + } + + @Test + public void testEvaluateArray() throws Exception { + String json = "{ \"field1\": { \"array1\" : [ \"value1\", \"value2\" ] } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.array1"); + assertThat(object, instanceOf(List.class)); + List list = (List) object; + assertThat(list.size(), equalTo(2)); + assertThat(list.get(0), instanceOf(String.class)); + assertThat((String)list.get(0), equalTo("value1")); + assertThat(list.get(1), instanceOf(String.class)); + assertThat((String)list.get(1), equalTo("value2")); + } + + @Test + public void testEvaluateArrayElement() throws Exception { + String json = "{ \"field1\": { \"array1\" : [ \"value1\", \"value2\" ] } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.array1.1"); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateArrayElementObject() throws Exception { + String json = "{ \"field1\": { \"array1\" : [ {\"element\": \"value1\"}, {\"element\":\"value2\"} ] } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.array1.1.element"); + assertThat(object, instanceOf(String.class)); + assertThat((String)object, equalTo("value2")); + } + + @Test + public void testEvaluateArrayElementObjectWrongPath() throws Exception { + String json = "{ \"field1\": { \"array1\" : [ {\"element\": \"value1\"}, {\"element\":\"value2\"} ] } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("field1.array2.1.element"); + assertThat(object, nullValue()); + } + + @Test + @SuppressWarnings("unchecked") + public void testEvaluateObjectKeys() throws Exception { + String json = "{ \"metadata\": { \"templates\" : {\"template_1\": { \"field\" : \"value\"}, \"template_2\": { \"field\" : \"value\"} } } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate("metadata.templates"); + assertThat(object, instanceOf(Map.class)); + Map map = (Map)object; + assertThat(map.size(), equalTo(2)); + Set strings = map.keySet(); + assertThat(strings, contains("template_1", "template_2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testEvaluateEmptyPath() throws Exception { + String json = "{ \"field1\": { \"array1\" : [ {\"element\": \"value1\"}, {\"element\":\"value2\"} ] } }"; + JsonPath jsonPath = new JsonPath(json); + Object object = jsonPath.evaluate(""); + assertThat(object, notNullValue()); + assertThat(object, instanceOf(Map.class)); + assertThat(((Map)object).containsKey("field1"), equalTo(true)); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/RestApiParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/RestApiParserTests.java new file mode 100644 index 000000000000..718598c7c8ef --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/RestApiParserTests.java @@ -0,0 +1,172 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.rest.spec.RestApi; +import org.elasticsearch.test.rest.spec.RestApiParser; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class RestApiParserTests extends AbstractParserTests { + + @Test + public void testParseRestSpecIndexApi() throws Exception { + parser = JsonXContent.jsonXContent.createParser(REST_SPEC_INDEX_API); + RestApi restApi = new RestApiParser().parse(parser); + + assertThat(restApi, notNullValue()); + assertThat(restApi.getName(), equalTo("index")); + assertThat(restApi.getMethods().size(), equalTo(2)); + assertThat(restApi.getMethods().get(0), equalTo("POST")); + assertThat(restApi.getMethods().get(1), equalTo("PUT")); + assertThat(restApi.getPaths().size(), equalTo(2)); + assertThat(restApi.getPaths().get(0), equalTo("/{index}/{type}")); + assertThat(restApi.getPaths().get(1), equalTo("/{index}/{type}/{id}")); + assertThat(restApi.getPathParts().size(), equalTo(3)); + assertThat(restApi.getPathParts().get(0), equalTo("id")); + assertThat(restApi.getPathParts().get(1), equalTo("index")); + assertThat(restApi.getPathParts().get(2), equalTo("type")); + } + + @Test + public void testParseRestSpecGetTemplateApi() throws Exception { + parser = JsonXContent.jsonXContent.createParser(REST_SPEC_GET_TEMPLATE_API); + RestApi restApi = new RestApiParser().parse(parser); + assertThat(restApi, notNullValue()); + assertThat(restApi.getName(), equalTo("indices.get_template")); + assertThat(restApi.getMethods().size(), equalTo(1)); + assertThat(restApi.getMethods().get(0), equalTo("GET")); + assertThat(restApi.getPaths().size(), equalTo(2)); + assertThat(restApi.getPaths().get(0), equalTo("/_template")); + assertThat(restApi.getPaths().get(1), equalTo("/_template/{name}")); + assertThat(restApi.getPathParts().size(), equalTo(1)); + assertThat(restApi.getPathParts().get(0), equalTo("name")); + } + + private static final String REST_SPEC_GET_TEMPLATE_API = "{\n" + + " \"indices.get_template\": {\n" + + " \"documentation\": \"http://www.elasticsearch.org/guide/reference/api/admin-indices-templates/\",\n" + + " \"methods\": [\"GET\"],\n" + + " \"url\": {\n" + + " \"path\": \"/_template/{name}\",\n" + + " \"paths\": [\"/_template\", \"/_template/{name}\"],\n" + + " \"parts\": {\n" + + " \"name\": {\n" + + " \"type\" : \"string\",\n" + + " \"required\" : false,\n" + + " \"description\" : \"The name of the template\"\n" + + " }\n" + + " },\n" + + " \"params\": {\n" + + " }\n" + + " },\n" + + " \"body\": null\n" + + " }\n" + + "}"; + + private static final String REST_SPEC_INDEX_API = "{\n" + + " \"index\": {\n" + + " \"documentation\": \"http://elasticsearch.org/guide/reference/api/index_/\",\n" + + " \"methods\": [\"POST\", \"PUT\"],\n" + + " \"url\": {\n" + + " \"path\": \"/{index}/{type}\",\n" + + " \"paths\": [\"/{index}/{type}\", \"/{index}/{type}/{id}\"],\n" + + " \"parts\": {\n" + + " \"id\": {\n" + + " \"type\" : \"string\",\n" + + " \"description\" : \"Document ID\"\n" + + " },\n" + + " \"index\": {\n" + + " \"type\" : \"string\",\n" + + " \"required\" : true,\n" + + " \"description\" : \"The name of the index\"\n" + + " },\n" + + " \"type\": {\n" + + " \"type\" : \"string\",\n" + + " \"required\" : true,\n" + + " \"description\" : \"The type of the document\"\n" + + " }\n" + + " } ,\n" + + " \"params\": {\n" + + " \"consistency\": {\n" + + " \"type\" : \"enum\",\n" + + " \"options\" : [\"one\", \"quorum\", \"all\"],\n" + + " \"description\" : \"Explicit write consistency setting for the operation\"\n" + + " },\n" + + " \"op_type\": {\n" + + " \"type\" : \"enum\",\n" + + " \"options\" : [\"index\", \"create\"],\n" + + " \"default\" : \"index\",\n" + + " \"description\" : \"Explicit operation type\"\n" + + " },\n" + + " \"parent\": {\n" + + " \"type\" : \"string\",\n" + + " \"description\" : \"ID of the parent document\"\n" + + " },\n" + + " \"percolate\": {\n" + + " \"type\" : \"string\",\n" + + " \"description\" : \"Percolator queries to execute while indexing the document\"\n" + + " },\n" + + " \"refresh\": {\n" + + " \"type\" : \"boolean\",\n" + + " \"description\" : \"Refresh the index after performing the operation\"\n" + + " },\n" + + " \"replication\": {\n" + + " \"type\" : \"enum\",\n" + + " \"options\" : [\"sync\",\"async\"],\n" + + " \"default\" : \"sync\",\n" + + " \"description\" : \"Specific replication type\"\n" + + " },\n" + + " \"routing\": {\n" + + " \"type\" : \"string\",\n" + + " \"description\" : \"Specific routing value\"\n" + + " },\n" + + " \"timeout\": {\n" + + " \"type\" : \"time\",\n" + + " \"description\" : \"Explicit operation timeout\"\n" + + " },\n" + + " \"timestamp\": {\n" + + " \"type\" : \"time\",\n" + + " \"description\" : \"Explicit timestamp for the document\"\n" + + " },\n" + + " \"ttl\": {\n" + + " \"type\" : \"duration\",\n" + + " \"description\" : \"Expiration time for the document\"\n" + + " },\n" + + " \"version\" : {\n" + + " \"type\" : \"number\",\n" + + " \"description\" : \"Explicit version number for concurrency control\"\n" + + " },\n" + + " \"version_type\": {\n" + + " \"type\" : \"enum\",\n" + + " \"options\" : [\"internal\",\"external\"],\n" + + " \"description\" : \"Specific version type\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"body\": {\n" + + " \"description\" : \"The document\",\n" + + " \"required\" : true\n" + + " }\n" + + " }\n" + + "}\n"; +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/RestTestParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/RestTestParserTests.java new file mode 100644 index 000000000000..04e1fe30fbd6 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/RestTestParserTests.java @@ -0,0 +1,501 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.parser.RestTestSuiteParser; +import org.elasticsearch.test.rest.section.DoSection; +import org.elasticsearch.test.rest.section.IsTrueAssertion; +import org.elasticsearch.test.rest.section.MatchAssertion; +import org.elasticsearch.test.rest.section.RestTestSuite; +import org.junit.After; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.Matchers.*; + +public class RestTestParserTests extends ElasticsearchTestCase { + + private XContentParser parser; + + @After + public void tearDown() throws Exception { + super.tearDown(); + //makes sure that we consumed the whole stream, XContentParser doesn't expose isClosed method + //next token can be null even in the middle of the document (e.g. with "---"), but not too many consecutive times + assertThat(parser.currentToken(), nullValue()); + assertThat(parser.nextToken(), nullValue()); + assertThat(parser.nextToken(), nullValue()); + parser.close(); + } + + @Test + public void testParseTestSetupAndSections() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "setup:\n" + + " - do:\n" + + " indices.create:\n" + + " index: test_index\n" + + "\n" + + "---\n" + + "\"Get index mapping\":\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + "\n" + + " - match: {test_index.test_type.properties.text.type: string}\n" + + " - match: {test_index.test_type.properties.text.analyzer: whitespace}\n" + + "\n" + + "---\n" + + "\"Get type mapping - pre 1.0\":\n" + + "\n" + + " - skip:\n" + + " version: \"0.90.9 - 999\"\n" + + " reason: \"for newer versions the index name is always returned\"\n" + + "\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + " type: test_type\n" + + "\n" + + " - match: {test_type.properties.text.type: string}\n" + + " - match: {test_type.properties.text.analyzer: whitespace}\n" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + assertThat(restTestSuite.getSetupSection(), notNullValue()); + assertThat(restTestSuite.getSetupSection().getSkipSection().isEmpty(), equalTo(true)); + + assertThat(restTestSuite.getSetupSection().getDoSections().size(), equalTo(1)); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getApi(), equalTo("indices.create")); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getParams().size(), equalTo(1)); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getParams().get("index"), equalTo("test_index")); + + assertThat(restTestSuite.getTestSections().size(), equalTo(2)); + + assertThat(restTestSuite.getTestSections().get(0).getName(), equalTo("Get index mapping")); + assertThat(restTestSuite.getTestSections().get(0).getSkipSection().isEmpty(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().size(), equalTo(3)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(0), instanceOf(DoSection.class)); + DoSection doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(0); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_mapping")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(1)); + assertThat(doSection.getApiCallSection().getParams().get("index"), equalTo("test_index")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(MatchAssertion.class)); + MatchAssertion matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(1); + assertThat(matchAssertion.getField(), equalTo("test_index.test_type.properties.text.type")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("string")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(2), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(2); + assertThat(matchAssertion.getField(), equalTo("test_index.test_type.properties.text.analyzer")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("whitespace")); + + assertThat(restTestSuite.getTestSections().get(1).getName(), equalTo("Get type mapping - pre 1.0")); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().isEmpty(), equalTo(false)); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getReason(), equalTo("for newer versions the index name is always returned")); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getVersion(), equalTo("0.90.9 - 999")); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().size(), equalTo(3)); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().get(0), instanceOf(DoSection.class)); + doSection = (DoSection) restTestSuite.getTestSections().get(1).getExecutableSections().get(0); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_mapping")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(2)); + assertThat(doSection.getApiCallSection().getParams().get("index"), equalTo("test_index")); + assertThat(doSection.getApiCallSection().getParams().get("type"), equalTo("test_type")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(1).getExecutableSections().get(1); + assertThat(matchAssertion.getField(), equalTo("test_type.properties.text.type")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("string")); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().get(2), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(1).getExecutableSections().get(2); + assertThat(matchAssertion.getField(), equalTo("test_type.properties.text.analyzer")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("whitespace")); + } + + @Test + public void testParseTestSetupAndSectionsSkipLastSection() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "setup:\n" + + " - do:\n" + + " indices.create:\n" + + " index: test_index\n" + + "\n" + + "---\n" + + "\"Get index mapping\":\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + "\n" + + " - match: {test_index.test_type.properties.text.type: string}\n" + + " - match: {test_index.test_type.properties.text.analyzer: whitespace}\n" + + "\n" + + "---\n" + + "\"Get type mapping - pre 1.0\":\n" + + "\n" + + " - skip:\n" + + " version: \"0.90.9 - 999\"\n" + + " reason: \"for newer versions the index name is always returned\"\n" + + "\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + " type: test_type\n" + + "\n" + + " - match: {test_type.properties.text.type: string}\n" + + " - match: {test_type.properties.text.analyzer: whitespace}\n" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "1.0.0")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + assertThat(restTestSuite.getSetupSection(), notNullValue()); + assertThat(restTestSuite.getSetupSection().getSkipSection().isEmpty(), equalTo(true)); + + assertThat(restTestSuite.getSetupSection().getDoSections().size(), equalTo(1)); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getApi(), equalTo("indices.create")); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getParams().size(), equalTo(1)); + assertThat(restTestSuite.getSetupSection().getDoSections().get(0).getApiCallSection().getParams().get("index"), equalTo("test_index")); + + assertThat(restTestSuite.getTestSections().size(), equalTo(2)); + + assertThat(restTestSuite.getTestSections().get(0).getName(), equalTo("Get index mapping")); + assertThat(restTestSuite.getTestSections().get(0).getSkipSection().isEmpty(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().size(), equalTo(3)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(0), instanceOf(DoSection.class)); + DoSection doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(0); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_mapping")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(1)); + assertThat(doSection.getApiCallSection().getParams().get("index"), equalTo("test_index")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(MatchAssertion.class)); + MatchAssertion matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(1); + assertThat(matchAssertion.getField(), equalTo("test_index.test_type.properties.text.type")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("string")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(2), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(2); + assertThat(matchAssertion.getField(), equalTo("test_index.test_type.properties.text.analyzer")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("whitespace")); + + assertThat(restTestSuite.getTestSections().get(1).getName(), equalTo("Get type mapping - pre 1.0")); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().isEmpty(), equalTo(false)); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getReason(), equalTo("for newer versions the index name is always returned")); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().getVersion(), equalTo("0.90.9 - 999")); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().size(), equalTo(0)); + } + + @Test + public void testParseTestSetupAndSectionsSkipEntireFile() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "setup:\n" + + " - skip:\n" + + " version: \"0.90.3 - 0.90.6\"\n" + + " reason: \"test skip entire file\"\n" + + " - do:\n" + + " indices.create:\n" + + " index: test_index\n" + + "\n" + + "---\n" + + "\"Get index mapping\":\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + "\n" + + " - match: {test_index.test_type.properties.text.type: string}\n" + + " - match: {test_index.test_type.properties.text.analyzer: whitespace}\n" + + "\n" + + "---\n" + + "\"Get type mapping - pre 1.0\":\n" + + "\n" + + " - skip:\n" + + " version: \"0.90.9 - 999\"\n" + + " reason: \"for newer versions the index name is always returned\"\n" + + "\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + " type: test_type\n" + + "\n" + + " - match: {test_type.properties.text.type: string}\n" + + " - match: {test_type.properties.text.analyzer: whitespace}\n" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + assertThat(restTestSuite.getSetupSection(), notNullValue()); + + assertThat(restTestSuite.getSetupSection().getSkipSection().isEmpty(), equalTo(false)); + assertThat(restTestSuite.getSetupSection().getSkipSection().getVersion(), equalTo("0.90.3 - 0.90.6")); + assertThat(restTestSuite.getSetupSection().getSkipSection().getReason(), equalTo("test skip entire file")); + + assertThat(restTestSuite.getSetupSection().getDoSections().size(), equalTo(0)); + + assertThat(restTestSuite.getTestSections().size(), equalTo(0)); + } + + @Test + public void testParseTestSetupAndSectionsSkipEntireFileNoDo() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "setup:\n" + + " - skip:\n" + + " version: \"0.90.3 - 0.90.6\"\n" + + " reason: \"test skip entire file\"\n" + + "\n" + + "---\n" + + "\"Get index mapping\":\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + "\n" + + " - match: {test_index.test_type.properties.text.type: string}\n" + + " - match: {test_index.test_type.properties.text.analyzer: whitespace}\n" + + "\n" + + "---\n" + + "\"Get type mapping - pre 1.0\":\n" + + "\n" + + " - skip:\n" + + " version: \"0.90.9 - 999\"\n" + + " reason: \"for newer versions the index name is always returned\"\n" + + "\n" + + " - do:\n" + + " indices.get_mapping:\n" + + " index: test_index\n" + + " type: test_type\n" + + "\n" + + " - match: {test_type.properties.text.type: string}\n" + + " - match: {test_type.properties.text.analyzer: whitespace}\n" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + assertThat(restTestSuite.getSetupSection(), notNullValue()); + + assertThat(restTestSuite.getSetupSection().getSkipSection().isEmpty(), equalTo(false)); + assertThat(restTestSuite.getSetupSection().getSkipSection().getVersion(), equalTo("0.90.3 - 0.90.6")); + assertThat(restTestSuite.getSetupSection().getSkipSection().getReason(), equalTo("test skip entire file")); + + assertThat(restTestSuite.getSetupSection().getDoSections().size(), equalTo(0)); + + assertThat(restTestSuite.getTestSections().size(), equalTo(0)); + } + + @Test + public void testParseTestSingleTestSection() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "---\n" + + "\"Index with ID\":\n" + + "\n" + + " - do:\n" + + " index:\n" + + " index: test-weird-index-中文\n" + + " type: weird.type\n" + + " id: 1\n" + + " body: { foo: bar }\n" + + "\n" + + " - is_true: ok\n" + + " - match: { _index: test-weird-index-中文 }\n" + + " - match: { _type: weird.type }\n" + + " - match: { _id: \"1\"}\n" + + " - match: { _version: 1}\n" + + "\n" + + " - do:\n" + + " get:\n" + + " index: test-weird-index-中文\n" + + " type: weird.type\n" + + " id: 1\n" + + "\n" + + " - match: { _index: test-weird-index-中文 }\n" + + " - match: { _type: weird.type }\n" + + " - match: { _id: \"1\"}\n" + + " - match: { _version: 1}\n" + + " - match: { _source: { foo: bar }}" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + + assertThat(restTestSuite.getSetupSection().isEmpty(), equalTo(true)); + + assertThat(restTestSuite.getTestSections().size(), equalTo(1)); + + assertThat(restTestSuite.getTestSections().get(0).getName(), equalTo("Index with ID")); + assertThat(restTestSuite.getTestSections().get(0).getSkipSection().isEmpty(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().size(), equalTo(12)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(0), instanceOf(DoSection.class)); + DoSection doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(0); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("index")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(IsTrueAssertion.class)); + IsTrueAssertion trueAssertion = (IsTrueAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(1); + assertThat(trueAssertion.getField(), equalTo("ok")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(2), instanceOf(MatchAssertion.class)); + MatchAssertion matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(2); + assertThat(matchAssertion.getField(), equalTo("_index")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("test-weird-index-中文")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(3), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(3); + assertThat(matchAssertion.getField(), equalTo("_type")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("weird.type")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(4), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(4); + assertThat(matchAssertion.getField(), equalTo("_id")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("1")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(5), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(5); + assertThat(matchAssertion.getField(), equalTo("_version")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("1")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(6), instanceOf(DoSection.class)); + doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(6); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("get")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(7), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(7); + assertThat(matchAssertion.getField(), equalTo("_index")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("test-weird-index-中文")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(8), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(8); + assertThat(matchAssertion.getField(), equalTo("_type")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("weird.type")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(9), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(9); + assertThat(matchAssertion.getField(), equalTo("_id")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("1")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(10), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(10); + assertThat(matchAssertion.getField(), equalTo("_version")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("1")); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(11), instanceOf(MatchAssertion.class)); + matchAssertion = (MatchAssertion) restTestSuite.getTestSections().get(0).getExecutableSections().get(11); + assertThat(matchAssertion.getField(), equalTo("_source")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(Map.class)); + assertThat(((Map) matchAssertion.getExpectedValue()).get("foo").toString(), equalTo("bar")); + } + + @Test + public void testParseTestMultipleTestSections() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "---\n" + + "\"Missing document (partial doc)\":\n" + + "\n" + + " - do:\n" + + " catch: missing\n" + + " update:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: { doc: { foo: bar } }\n" + + "\n" + + " - do:\n" + + " update:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: { doc: { foo: bar } }\n" + + " ignore: 404\n" + + "\n" + + "---\n" + + "\"Missing document (script)\":\n" + + "\n" + + "\n" + + " - do:\n" + + " catch: missing\n" + + " update:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body:\n" + + " script: \"ctx._source.foo = bar\"\n" + + " params: { bar: 'xxx' }\n" + + "\n" + + " - do:\n" + + " update:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " ignore: 404\n" + + " body:\n" + + " script: \"ctx._source.foo = bar\"\n" + + " params: { bar: 'xxx' }\n" + ); + + RestTestSuiteParser testParser = new RestTestSuiteParser(); + RestTestSuite restTestSuite = testParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo("suite")); + + assertThat(restTestSuite.getSetupSection().isEmpty(), equalTo(true)); + + assertThat(restTestSuite.getTestSections().size(), equalTo(2)); + + assertThat(restTestSuite.getTestSections().get(0).getName(), equalTo("Missing document (partial doc)")); + assertThat(restTestSuite.getTestSections().get(0).getSkipSection().isEmpty(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().size(), equalTo(2)); + + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(0), instanceOf(DoSection.class)); + DoSection doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(0); + assertThat(doSection.getCatch(), equalTo("missing")); + assertThat(doSection.getApiCallSection().getApi(), equalTo("update")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(DoSection.class)); + doSection = (DoSection) restTestSuite.getTestSections().get(0).getExecutableSections().get(1); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("update")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(4)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + + assertThat(restTestSuite.getTestSections().get(1).getName(), equalTo("Missing document (script)")); + assertThat(restTestSuite.getTestSections().get(1).getSkipSection().isEmpty(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().size(), equalTo(2)); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().get(0), instanceOf(DoSection.class)); + assertThat(restTestSuite.getTestSections().get(1).getExecutableSections().get(1), instanceOf(DoSection.class)); + doSection = (DoSection) restTestSuite.getTestSections().get(1).getExecutableSections().get(0); + assertThat(doSection.getCatch(), equalTo("missing")); + assertThat(doSection.getApiCallSection().getApi(), equalTo("update")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + assertThat(restTestSuite.getTestSections().get(0).getExecutableSections().get(1), instanceOf(DoSection.class)); + doSection = (DoSection) restTestSuite.getTestSections().get(1).getExecutableSections().get(1); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("update")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(4)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/SetSectionParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/SetSectionParserTests.java new file mode 100644 index 000000000000..ee0a16290dec --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/SetSectionParserTests.java @@ -0,0 +1,77 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.RestTestParseException; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.parser.SetSectionParser; +import org.elasticsearch.test.rest.section.SetSection; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class SetSectionParserTests extends AbstractParserTests { + + @Test + public void testParseSetSectionSingleValue() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ _id: id }" + ); + + SetSectionParser setSectionParser = new SetSectionParser(); + + SetSection setSection = setSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(setSection, notNullValue()); + assertThat(setSection.getStash(), notNullValue()); + assertThat(setSection.getStash().size(), equalTo(1)); + assertThat(setSection.getStash().get("_id"), equalTo("id")); + } + + @Test + public void testParseSetSectionMultipleValues() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ _id: id, _type: type, _index: index }" + ); + + SetSectionParser setSectionParser = new SetSectionParser(); + + SetSection setSection = setSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(setSection, notNullValue()); + assertThat(setSection.getStash(), notNullValue()); + assertThat(setSection.getStash().size(), equalTo(3)); + assertThat(setSection.getStash().get("_id"), equalTo("id")); + assertThat(setSection.getStash().get("_type"), equalTo("type")); + assertThat(setSection.getStash().get("_index"), equalTo("index")); + } + + @Test(expected = RestTestParseException.class) + public void testParseSetSectionNoValues() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "{ }" + ); + + SetSectionParser setSectionParser = new SetSectionParser(); + + setSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/rest/test/SetupSectionParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/SetupSectionParserTests.java new file mode 100644 index 000000000000..9043e297634a --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/SetupSectionParserTests.java @@ -0,0 +1,125 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.parser.SetupSectionParser; +import org.elasticsearch.test.rest.section.SetupSection; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class SetupSectionParserTests extends AbstractParserTests { + + @Test + public void testParseSetupSection() throws Exception { + + parser = YamlXContent.yamlXContent.createParser( + " - do:\n" + + " index1:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + + " - do:\n" + + " index2:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 2\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + ); + + SetupSectionParser setupSectionParser = new SetupSectionParser(); + SetupSection setupSection = setupSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(setupSection, notNullValue()); + assertThat(setupSection.getSkipSection().isEmpty(), equalTo(true)); + assertThat(setupSection.getDoSections().size(), equalTo(2)); + assertThat(setupSection.getDoSections().get(0).getApiCallSection().getApi(), equalTo("index1")); + assertThat(setupSection.getDoSections().get(1).getApiCallSection().getApi(), equalTo("index2")); + } + + @Test + public void testParseSetupAndSkipSectionSkip() throws Exception { + + parser = YamlXContent.yamlXContent.createParser( + " - skip:\n" + + " version: \"0.90.0 - 0.90.7\"\n" + + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + + " - do:\n" + + " index1:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + + " - do:\n" + + " index2:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 2\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + ); + + SetupSectionParser setupSectionParser = new SetupSectionParser(); + SetupSection setupSection = setupSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(setupSection, notNullValue()); + assertThat(setupSection.getSkipSection().isEmpty(), equalTo(false)); + assertThat(setupSection.getSkipSection(), notNullValue()); + assertThat(setupSection.getSkipSection().getVersion(), equalTo("0.90.0 - 0.90.7")); + assertThat(setupSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); + assertThat(setupSection.getDoSections().size(), equalTo(0)); + } + + @Test + public void testParseSetupAndSkipSectionNoSkip() throws Exception { + + parser = YamlXContent.yamlXContent.createParser( + " - skip:\n" + + " version: \"0.90.0 - 0.90.7\"\n" + + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + + " - do:\n" + + " index1:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 1\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + + " - do:\n" + + " index2:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 2\n" + + " body: { \"include\": { \"field1\": \"v1\", \"field2\": \"v2\" }, \"count\": 1 }\n" + ); + + SetupSectionParser setupSectionParser = new SetupSectionParser(); + SetupSection setupSection = setupSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.8")); + + assertThat(setupSection, notNullValue()); + assertThat(setupSection.getSkipSection().isEmpty(), equalTo(false)); + assertThat(setupSection.getSkipSection(), notNullValue()); + assertThat(setupSection.getSkipSection().getVersion(), equalTo("0.90.0 - 0.90.7")); + assertThat(setupSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); + assertThat(setupSection.getDoSections().size(), equalTo(2)); + assertThat(setupSection.getDoSections().get(0).getApiCallSection().getApi(), equalTo("index1")); + assertThat(setupSection.getDoSections().get(1).getApiCallSection().getApi(), equalTo("index2")); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/SkipSectionParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/SkipSectionParserTests.java new file mode 100644 index 000000000000..87f40a49ab74 --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/SkipSectionParserTests.java @@ -0,0 +1,68 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.RestTestParseException; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.parser.SkipSectionParser; +import org.elasticsearch.test.rest.section.SkipSection; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class SkipSectionParserTests extends AbstractParserTests { + + @Test + public void testParseSkipSection() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "version: \"0 - 0.90.2\"\n" + + "reason: Delete ignores the parent param" + ); + + SkipSectionParser skipSectionParser = new SkipSectionParser(); + + SkipSection skipSection = skipSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(skipSection, notNullValue()); + assertThat(skipSection.getVersion(), equalTo("0 - 0.90.2")); + assertThat(skipSection.getReason(), equalTo("Delete ignores the parent param")); + } + + @Test(expected = RestTestParseException.class) + public void testParseSkipSectionNoReason() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "version: \"0 - 0.90.2\"\n" + ); + + SkipSectionParser skipSectionParser = new SkipSectionParser(); + skipSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + } + + @Test(expected = RestTestParseException.class) + public void testParseSkipSectionNoVersion() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "reason: Delete ignores the parent param\n" + ); + + SkipSectionParser skipSectionParser = new SkipSectionParser(); + skipSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + } +} \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/test/rest/test/TestSectionParserTests.java b/src/test/java/org/elasticsearch/test/rest/test/TestSectionParserTests.java new file mode 100644 index 000000000000..43ca15807f1c --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/TestSectionParserTests.java @@ -0,0 +1,274 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.rest.parser.RestTestSectionParser; +import org.elasticsearch.test.rest.parser.RestTestSuiteParseContext; +import org.elasticsearch.test.rest.section.*; +import org.junit.Test; + +import java.util.Map; + +import static org.hamcrest.Matchers.*; + +public class TestSectionParserTests extends AbstractParserTests { + + @Test + public void testParseTestSectionWithDoSection() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "\"First test section\": \n" + + " - do :\n" + + " catch: missing\n" + + " indices.get_warmer:\n" + + " index: test_index\n" + + " name: test_warmer" + ); + + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("First test section")); + assertThat(testSection.getSkipSection(), equalTo(SkipSection.EMPTY)); + assertThat(testSection.getExecutableSections().size(), equalTo(1)); + DoSection doSection = (DoSection)testSection.getExecutableSections().get(0); + assertThat(doSection.getCatch(), equalTo("missing")); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_warmer")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(2)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + } + + @Test + public void testParseTestSectionWithDoSetAndSkipSectionsSkip() throws Exception { + String yaml = + "\"First test section\": \n" + + " - skip:\n" + + " version: \"0.90.0 - 0.90.7\"\n" + + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + + " - do :\n" + + " catch: missing\n" + + " indices.get_warmer:\n" + + " index: test_index\n" + + " name: test_warmer\n" + + " - set: {_scroll_id: scroll_id}"; + + + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + parser = YamlXContent.yamlXContent.createParser(yaml); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.7")); + + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("First test section")); + assertThat(testSection.getSkipSection(), notNullValue()); + assertThat(testSection.getSkipSection().getVersion(), equalTo("0.90.0 - 0.90.7")); + assertThat(testSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); + //skip parsing when needed + assertThat(testSection.getExecutableSections().size(), equalTo(0)); + } + + @Test + public void testParseTestSectionWithDoSetAndSkipSectionsNoSkip() throws Exception { + String yaml = + "\"First test section\": \n" + + " - skip:\n" + + " version: \"0.90.0 - 0.90.7\"\n" + + " reason: \"Update doesn't return metadata fields, waiting for #3259\"\n" + + " - do :\n" + + " catch: missing\n" + + " indices.get_warmer:\n" + + " index: test_index\n" + + " name: test_warmer\n" + + " - set: {_scroll_id: scroll_id}"; + + + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + parser = YamlXContent.yamlXContent.createParser(yaml); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.8")); + + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("First test section")); + assertThat(testSection.getSkipSection(), notNullValue()); + assertThat(testSection.getSkipSection().getVersion(), equalTo("0.90.0 - 0.90.7")); + assertThat(testSection.getSkipSection().getReason(), equalTo("Update doesn't return metadata fields, waiting for #3259")); + assertThat(testSection.getExecutableSections().size(), equalTo(2)); + DoSection doSection = (DoSection)testSection.getExecutableSections().get(0); + assertThat(doSection.getCatch(), equalTo("missing")); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("indices.get_warmer")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(2)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + SetSection setSection = (SetSection) testSection.getExecutableSections().get(1); + assertThat(setSection.getStash().size(), equalTo(1)); + assertThat(setSection.getStash().get("_scroll_id"), equalTo("scroll_id")); + } + + @Test + public void testParseTestSectionWithMultipleDoSections() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "\"Basic\":\n" + + "\n" + + " - do:\n" + + " index:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 中文\n" + + " body: { \"foo\": \"Hello: 中文\" }\n" + + " - do:\n" + + " get:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 中文" + ); + + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("Basic")); + assertThat(testSection.getSkipSection(), equalTo(SkipSection.EMPTY)); + assertThat(testSection.getExecutableSections().size(), equalTo(2)); + DoSection doSection = (DoSection)testSection.getExecutableSections().get(0); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("index")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + doSection = (DoSection)testSection.getExecutableSections().get(1); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("get")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + } + + @Test + public void testParseTestSectionWithDoSectionsAndAssertions() throws Exception { + parser = YamlXContent.yamlXContent.createParser( + "\"Basic\":\n" + + "\n" + + " - do:\n" + + " index:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 中文\n" + + " body: { \"foo\": \"Hello: 中文\" }\n" + + "\n" + + " - do:\n" + + " get:\n" + + " index: test_1\n" + + " type: test\n" + + " id: 中文\n" + + "\n" + + " - match: { _index: test_1 }\n" + + " - is_true: _source\n" + + " - match: { _source: { foo: \"Hello: 中文\" } }\n" + + "\n" + + " - do:\n" + + " get:\n" + + " index: test_1\n" + + " id: 中文\n" + + "\n" + + " - length: { _index: 6 }\n" + + " - is_false: whatever\n" + + " - gt: { size: 5 }\n" + + " - lt: { size: 10 }" + ); + + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("Basic")); + assertThat(testSection.getSkipSection(), equalTo(SkipSection.EMPTY)); + assertThat(testSection.getExecutableSections().size(), equalTo(10)); + + DoSection doSection = (DoSection)testSection.getExecutableSections().get(0); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("index")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(true)); + + doSection = (DoSection)testSection.getExecutableSections().get(1); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("get")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(3)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + + MatchAssertion matchAssertion = (MatchAssertion)testSection.getExecutableSections().get(2); + assertThat(matchAssertion.getField(), equalTo("_index")); + assertThat(matchAssertion.getExpectedValue().toString(), equalTo("test_1")); + + IsTrueAssertion trueAssertion = (IsTrueAssertion)testSection.getExecutableSections().get(3); + assertThat(trueAssertion.getField(), equalTo("_source")); + + matchAssertion = (MatchAssertion)testSection.getExecutableSections().get(4); + assertThat(matchAssertion.getField(), equalTo("_source")); + assertThat(matchAssertion.getExpectedValue(), instanceOf(Map.class)); + Map map = (Map) matchAssertion.getExpectedValue(); + assertThat(map.size(), equalTo(1)); + assertThat(map.get("foo").toString(), equalTo("Hello: 中文")); + + doSection = (DoSection)testSection.getExecutableSections().get(5); + assertThat(doSection.getCatch(), nullValue()); + assertThat(doSection.getApiCallSection(), notNullValue()); + assertThat(doSection.getApiCallSection().getApi(), equalTo("get")); + assertThat(doSection.getApiCallSection().getParams().size(), equalTo(2)); + assertThat(doSection.getApiCallSection().hasBody(), equalTo(false)); + + LengthAssertion lengthAssertion = (LengthAssertion) testSection.getExecutableSections().get(6); + assertThat(lengthAssertion.getField(), equalTo("_index")); + assertThat(lengthAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) lengthAssertion.getExpectedValue(), equalTo(6)); + + IsFalseAssertion falseAssertion = (IsFalseAssertion)testSection.getExecutableSections().get(7); + assertThat(falseAssertion.getField(), equalTo("whatever")); + + GreaterThanAssertion greaterThanAssertion = (GreaterThanAssertion) testSection.getExecutableSections().get(8); + assertThat(greaterThanAssertion.getField(), equalTo("size")); + assertThat(greaterThanAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) greaterThanAssertion.getExpectedValue(), equalTo(5)); + + LessThanAssertion lessThanAssertion = (LessThanAssertion) testSection.getExecutableSections().get(9); + assertThat(lessThanAssertion.getField(), equalTo("size")); + assertThat(lessThanAssertion.getExpectedValue(), instanceOf(Integer.class)); + assertThat((Integer) lessThanAssertion.getExpectedValue(), equalTo(10)); + } + + @Test + public void testSmallSection() throws Exception { + + parser = YamlXContent.yamlXContent.createParser( + "\"node_info test\":\n" + + " - do:\n" + + " cluster.node_info: {}\n" + + " \n" + + " - is_true: ok\n" + + " - is_true: nodes\n" + + " - is_true: cluster_name\n"); + RestTestSectionParser testSectionParser = new RestTestSectionParser(); + TestSection testSection = testSectionParser.parse(new RestTestSuiteParseContext("api", "suite", parser, "0.90.5")); + assertThat(testSection, notNullValue()); + assertThat(testSection.getName(), equalTo("node_info test")); + assertThat(testSection.getExecutableSections().size(), equalTo(4)); + } +} diff --git a/src/test/java/org/elasticsearch/test/rest/test/VersionUtilsTests.java b/src/test/java/org/elasticsearch/test/rest/test/VersionUtilsTests.java new file mode 100644 index 000000000000..b05acb58d61b --- /dev/null +++ b/src/test/java/org/elasticsearch/test/rest/test/VersionUtilsTests.java @@ -0,0 +1,120 @@ +/* + * Licensed to ElasticSearch and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. ElasticSearch licenses this + * file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package org.elasticsearch.test.rest.test; + +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import static org.elasticsearch.test.rest.support.VersionUtils.parseVersionNumber; +import static org.elasticsearch.test.rest.support.VersionUtils.skipCurrentVersion; +import static org.hamcrest.Matchers.*; + +public class VersionUtilsTests extends ElasticsearchTestCase { + + @Test + public void testParseVersionNumber() { + + int[] versionNumber = parseVersionNumber("0.90.6"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(0)); + assertThat(versionNumber[1], equalTo(90)); + assertThat(versionNumber[2], equalTo(6)); + + versionNumber = parseVersionNumber("0.90.999"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(0)); + assertThat(versionNumber[1], equalTo(90)); + assertThat(versionNumber[2], equalTo(999)); + + versionNumber = parseVersionNumber("0.20.11"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(0)); + assertThat(versionNumber[1], equalTo(20)); + assertThat(versionNumber[2], equalTo(11)); + + versionNumber = parseVersionNumber("1.0.0.Beta1"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(1)); + assertThat(versionNumber[1], equalTo(0)); + assertThat(versionNumber[2], equalTo(0)); + + versionNumber = parseVersionNumber("1.0.0.RC1"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(1)); + assertThat(versionNumber[1], equalTo(0)); + assertThat(versionNumber[2], equalTo(0)); + + versionNumber = parseVersionNumber("1.0.0"); + assertThat(versionNumber.length, equalTo(3)); + assertThat(versionNumber[0], equalTo(1)); + assertThat(versionNumber[1], equalTo(0)); + assertThat(versionNumber[2], equalTo(0)); + + versionNumber = parseVersionNumber("1.0"); + assertThat(versionNumber.length, equalTo(2)); + assertThat(versionNumber[0], equalTo(1)); + assertThat(versionNumber[1], equalTo(0)); + + versionNumber = parseVersionNumber("999"); + assertThat(versionNumber.length, equalTo(1)); + assertThat(versionNumber[0], equalTo(999)); + + versionNumber = parseVersionNumber("0"); + assertThat(versionNumber.length, equalTo(1)); + assertThat(versionNumber[0], equalTo(0)); + + try { + parseVersionNumber("1.0.Beta1"); + fail("parseVersionNumber should have thrown an error"); + } catch(IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("version is not a number")); + assertThat(e.getCause(), instanceOf(NumberFormatException.class)); + } + } + + @Test + public void testSkipCurrentVersion() { + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.90.2"), equalTo(true)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.90.3"), equalTo(true)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.90.6"), equalTo(true)); + + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.20.10"), equalTo(false)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.90.1"), equalTo(false)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "0.90.7"), equalTo(false)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.6", "1.0.0"), equalTo(false)); + + assertThat(skipCurrentVersion(" 0.90.2 - 0.90.999 ", "0.90.15"), equalTo(true)); + assertThat(skipCurrentVersion("0.90.2 - 0.90.999", "1.0.0"), equalTo(false)); + + assertThat(skipCurrentVersion("0 - 999", "0.90.15"), equalTo(true)); + assertThat(skipCurrentVersion("0 - 999", "0.20.1"), equalTo(true)); + assertThat(skipCurrentVersion("0 - 999", "1.0.0"), equalTo(true)); + + assertThat(skipCurrentVersion("0.90.9 - 999", "1.0.0"), equalTo(true)); + assertThat(skipCurrentVersion("0.90.9 - 999", "0.90.8"), equalTo(false)); + + try { + assertThat(skipCurrentVersion("0.90.2 - 0.90.999 - 1.0.0", "1.0.0"), equalTo(false)); + fail("skipCurrentVersion should have thrown an error"); + } catch(IllegalArgumentException e) { + assertThat(e.getMessage(), containsString("too many skip versions found")); + } + + } +} diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties index 526e22d4de0e..22f54ef68e52 100644 --- a/src/test/resources/log4j.properties +++ b/src/test/resources/log4j.properties @@ -1,6 +1,9 @@ es.logger.level=INFO log4j.rootLogger=${es.logger.level}, out +log4j.logger.org.apache.http=INFO, out +log4j.additivity.org.apache.http=false + log4j.appender.out=org.apache.log4j.ConsoleAppender log4j.appender.out.layout=org.apache.log4j.PatternLayout log4j.appender.out.layout.conversionPattern=[%d{ISO8601}][%-5p][%-25c] %m%n