Files
opensearch-pyd/test_elasticsearch/test_server/test_rest_api_spec.py
T

448 lines
15 KiB
Python
Raw Normal View History

# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. 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.
2020-04-23 11:22:08 -05:00
"""
2020-05-14 16:09:24 -05:00
Dynamically generated set of TestCases based on set of yaml files describing
some integration tests. These files are shared among all official Elasticsearch
clients.
"""
2020-05-15 09:37:49 -05:00
import sys
import re
2020-05-15 09:37:49 -05:00
import os
from os import walk, environ
2020-05-14 16:09:24 -05:00
from os.path import exists, join, dirname, pardir, relpath
import yaml
2016-03-22 21:04:07 +01:00
from shutil import rmtree
import warnings
2020-05-14 16:09:24 -05:00
import pytest
from elasticsearch import TransportError, RequestError, ElasticsearchDeprecationWarning
2014-02-21 16:53:56 +01:00
from elasticsearch.compat import string_types
from elasticsearch.helpers.test import _get_version
2013-11-14 01:08:19 +01:00
2013-06-16 16:04:00 +02:00
# some params had to be changed in python, keep track of them so we can rename
# those in the tests accordingly
2019-03-29 09:25:23 -06:00
PARAMS_RENAMES = {"type": "doc_type", "from": "from_"}
2013-06-16 16:04:00 +02:00
2013-11-14 01:08:19 +01:00
# mapping from catch values to http status codes
2019-03-29 09:25:23 -06:00
CATCH_CODES = {"missing": 404, "conflict": 409, "unauthorized": 401}
2014-02-03 19:10:58 +01:00
# test features we have implemented
2020-03-09 11:51:35 -05:00
IMPLEMENTED_FEATURES = {
"gtelte",
"stash_in_path",
"headers",
"catch_unauthorized",
"default_shards",
"warnings",
"allowed_warnings",
2020-03-09 11:51:35 -05:00
}
2014-02-03 19:10:58 +01:00
# broken YAML tests on some releases
SKIP_TESTS = {
2020-05-14 16:09:24 -05:00
# can't figure out the expand_wildcards=open issue?
"indices/get_alias/10_basic[23]",
# [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future.
"search/aggregation/230_composite[6]",
"search/aggregation/250_moving_fn[1]",
2020-05-15 09:37:49 -05:00
"search/aggregation/250_moving_fn[2]",
2020-05-14 16:09:24 -05:00
# fails by not returning 'search'?
"search/320_disallow_queries[2]",
"search/40_indices_boost[1]",
# ?q= fails
"explain/30_query_string[0]",
"count/20_query_string[0]",
# index template issues
"indices/put_template/10_basic[0]",
"indices/put_template/10_basic[1]",
"indices/put_template/10_basic[2]",
"indices/put_template/10_basic[3]",
"indices/put_template/10_basic[4]",
# depends on order of response JSON which is random
"indices/simulate_index_template/10_basic[1]",
2020-05-15 09:37:49 -05:00
# body: null? body is {}
"indices/simulate_index_template/10_basic[2]",
}
2020-04-03 12:52:11 -05:00
2020-01-19 00:46:24 +00:00
XPACK_FEATURES = None
2020-05-14 16:09:24 -05:00
ES_VERSION = None
2020-05-15 09:37:49 -05:00
RUN_ASYNC_REST_API_TESTS = (
sys.version_info >= (3, 6)
and os.environ.get("PYTHON_CONNECTION_CLASS") == "RequestsHttpConnection"
)
2020-01-19 00:46:24 +00:00
2019-03-29 09:25:23 -06:00
2020-05-14 16:09:24 -05:00
class YamlRunner:
def __init__(self, client):
self.client = client
self.last_response = None
2020-05-14 16:09:24 -05:00
self._run_code = None
self._setup_code = None
self._teardown_code = None
2013-07-10 16:43:20 +02:00
self._state = {}
2020-05-14 16:09:24 -05:00
def use_spec(self, test_spec):
self._setup_code = test_spec.pop("setup", None)
self._run_code = test_spec.pop("run", None)
2020-05-15 09:37:49 -05:00
self._teardown_code = test_spec.pop("teardown", None)
2020-05-14 16:09:24 -05:00
def setup(self):
if self._setup_code:
self.run_code(self._setup_code)
def teardown(self):
if self._teardown_code:
self.run_code(self._teardown_code)
2020-05-14 16:09:24 -05:00
for repo, definition in (
self.client.snapshot.get_repository(repository="_all")
2019-03-29 09:25:23 -06:00
).items():
2016-03-22 21:04:07 +01:00
self.client.snapshot.delete_repository(repository=repo)
2019-03-29 09:25:23 -06:00
if definition["type"] == "fs":
rmtree(
"/tmp/%s" % definition["settings"]["location"], ignore_errors=True
)
2016-03-22 21:04:07 +01:00
2020-01-19 00:46:24 +00:00
# stop and remove all ML stuff
if self._feature_enabled("ml"):
self.client.ml.stop_datafeed(datafeed_id="*", force=True)
2020-05-14 16:09:24 -05:00
for feed in (self.client.ml.get_datafeeds(datafeed_id="*"))["datafeeds"]:
2020-01-19 00:46:24 +00:00
self.client.ml.delete_datafeed(datafeed_id=feed["datafeed_id"])
self.client.ml.close_job(job_id="*", force=True)
2020-05-14 16:09:24 -05:00
for job in (self.client.ml.get_jobs(job_id="*"))["jobs"]:
2020-01-19 00:46:24 +00:00
self.client.ml.delete_job(
job_id=job["job_id"], wait_for_completion=True, force=True
)
# stop and remove all Rollup jobs
if self._feature_enabled("rollup"):
2020-05-14 16:09:24 -05:00
for rollup in (self.client.rollup.get_jobs(id="*"))["jobs"]:
2020-01-19 00:46:24 +00:00
self.client.rollup.stop_job(
id=rollup["config"]["id"], wait_for_completion=True
)
self.client.rollup.delete_job(id=rollup["config"]["id"])
2020-05-14 16:09:24 -05:00
def es_version(self):
global ES_VERSION
if ES_VERSION is None:
version_string = (self.client.info())["version"]["number"]
if "." not in version_string:
return ()
version = version_string.strip().split(".")
ES_VERSION = tuple(int(v) if v.isdigit() else 999 for v in version)
return ES_VERSION
2020-01-19 00:46:24 +00:00
2020-05-14 16:09:24 -05:00
def run(self):
try:
self.setup()
self.run_code(self._run_code)
finally:
self.teardown()
2013-07-10 17:03:48 +02:00
def run_code(self, test):
""" Execute an instruction based on it's type. """
print(test)
for action in test:
2020-05-14 16:09:24 -05:00
assert len(action) == 1
2013-06-14 17:27:32 +02:00
action_type, action = list(action.items())[0]
2019-03-29 09:25:23 -06:00
if hasattr(self, "run_" + action_type):
getattr(self, "run_" + action_type)(action)
else:
raise InvalidActionType(action_type)
def run_do(self, action):
api = self.client
2020-03-11 16:33:15 -05:00
headers = action.pop("headers", None)
2019-03-29 09:25:23 -06:00
catch = action.pop("catch", None)
2020-05-14 16:09:24 -05:00
warn = action.pop("warnings", ())
allowed_warnings = action.pop("allowed_warnings", ())
2020-05-14 16:09:24 -05:00
assert len(action) == 1
2013-06-14 17:27:32 +02:00
method, args = list(action.items())[0]
2020-03-11 16:33:15 -05:00
args["headers"] = headers
# locate api endpoint
2019-03-29 09:25:23 -06:00
for m in method.split("."):
2020-05-14 16:09:24 -05:00
assert hasattr(api, m)
api = getattr(api, m)
2013-06-16 16:04:00 +02:00
# some parameters had to be renamed to not clash with python builtins,
# compensate
for k in PARAMS_RENAMES:
if k in args:
args[PARAMS_RENAMES[k]] = args.pop(k)
2013-07-10 16:43:20 +02:00
# resolve vars
for k in args:
args[k] = self._resolve(args[k])
warnings.simplefilter("always", category=ElasticsearchDeprecationWarning)
with warnings.catch_warnings(record=True) as caught_warnings:
try:
self.last_response = api(**args)
except Exception as e:
if not catch:
raise
self.run_catch(catch, e)
else:
if catch:
raise AssertionError(
"Failed to catch %r in %r." % (catch, self.last_response)
)
# Filter out warnings raised by other components.
caught_warnings = [
str(w.message)
for w in caught_warnings
if w.category == ElasticsearchDeprecationWarning
and str(w.message) not in allowed_warnings
]
# Sorting removes the issue with order raised. We only care about
# if all warnings are raised in the single API call.
if warn and sorted(warn) != sorted(caught_warnings):
raise AssertionError(
"Expected warnings not equal to actual warnings: expected=%r actual=%r"
% (warn, caught_warnings)
)
2020-05-14 16:09:24 -05:00
def run_catch(self, catch, exception):
if catch == "param":
assert isinstance(exception, TypeError)
return
2020-05-14 16:09:24 -05:00
assert isinstance(exception, TransportError)
if catch in CATCH_CODES:
assert CATCH_CODES[catch] == exception.status_code
elif catch[0] == "/" and catch[-1] == "/":
assert (
re.search(catch[1:-1], exception.error + " " + repr(exception.info)),
"%s not in %r" % (catch, exception.info),
) is not None
self.last_response = exception.info
2013-07-11 02:00:28 +02:00
def run_skip(self, skip):
2020-05-14 16:09:24 -05:00
global IMPLEMENTED_FEATURES
2019-03-29 09:25:23 -06:00
if "features" in skip:
features = skip["features"]
2016-07-14 17:29:16 +02:00
if not isinstance(features, (tuple, list)):
features = [features]
for feature in features:
if feature in IMPLEMENTED_FEATURES:
continue
2020-05-14 16:09:24 -05:00
pytest.skip("feature '%s' is not supported" % feature)
2014-02-03 19:10:58 +01:00
2019-03-29 09:25:23 -06:00
if "version" in skip:
version, reason = skip["version"], skip["reason"]
if version == "all":
2020-05-14 16:09:24 -05:00
pytest.skip(reason)
2019-03-29 09:25:23 -06:00
min_version, max_version = version.split("-")
min_version = _get_version(min_version) or (0,)
max_version = _get_version(max_version) or (999,)
2020-05-14 16:09:24 -05:00
if min_version <= (self.es_version()) <= max_version:
pytest.skip(reason)
2013-07-10 17:03:48 +02:00
def run_gt(self, action):
for key, value in action.items():
2017-07-31 19:17:52 -04:00
value = self._resolve(value)
2020-05-14 16:09:24 -05:00
assert self._lookup(key) > value
2013-07-10 17:03:48 +02:00
2014-03-28 17:42:51 +01:00
def run_gte(self, action):
for key, value in action.items():
2017-07-31 19:17:52 -04:00
value = self._resolve(value)
2020-05-14 16:09:24 -05:00
assert self._lookup(key) >= value
2014-03-28 17:42:51 +01:00
2013-07-10 17:03:48 +02:00
def run_lt(self, action):
for key, value in action.items():
2017-07-31 19:17:52 -04:00
value = self._resolve(value)
2020-05-14 16:09:24 -05:00
assert self._lookup(key) < value
2013-07-10 16:43:20 +02:00
2014-03-28 17:42:51 +01:00
def run_lte(self, action):
for key, value in action.items():
2017-07-31 19:17:52 -04:00
value = self._resolve(value)
2020-05-14 16:09:24 -05:00
assert self._lookup(key) <= value
2014-03-28 17:42:51 +01:00
2013-07-10 16:43:20 +02:00
def run_set(self, action):
for key, value in action.items():
2017-07-31 19:17:52 -04:00
value = self._resolve(value)
2013-07-10 17:03:48 +02:00
self._state[value] = self._lookup(key)
2013-07-10 16:43:20 +02:00
def run_is_false(self, action):
try:
value = self._lookup(action)
except AssertionError:
pass
else:
2020-05-14 16:09:24 -05:00
assert value in ("", None, False, 0)
2013-07-10 16:43:20 +02:00
def run_is_true(self, action):
2013-07-10 17:03:48 +02:00
value = self._lookup(action)
2020-05-14 16:09:24 -05:00
assert value not in ("", None, False, 0)
2013-07-10 16:43:20 +02:00
def run_length(self, action):
2013-07-10 17:03:48 +02:00
for path, expected in action.items():
value = self._lookup(path)
expected = self._resolve(expected)
2020-05-14 16:09:24 -05:00
assert expected == len(value)
2013-07-10 17:03:48 +02:00
def run_match(self, action):
for path, expected in action.items():
value = self._lookup(path)
expected = self._resolve(expected)
2019-03-29 09:25:23 -06:00
if (
isinstance(expected, string_types)
and expected.startswith("/")
and expected.endswith("/")
):
2020-05-14 16:09:24 -05:00
expected = re.compile(expected[1:-1], re.VERBOSE | re.MULTILINE)
assert expected.search(value), "%r does not match %r" % (
value,
expected,
)
else:
2020-05-14 16:09:24 -05:00
assert expected == value, "%r does not match %r" % (value, expected)
2020-05-14 16:09:24 -05:00
def _resolve(self, value):
# resolve variables
if isinstance(value, string_types) and value.startswith("$"):
value = value[1:]
assert value in self._state
value = self._state[value]
if isinstance(value, string_types):
value = value.strip()
elif isinstance(value, dict):
value = dict((k, self._resolve(v)) for (k, v) in value.items())
elif isinstance(value, list):
value = list(map(self._resolve, value))
return value
2020-05-14 16:09:24 -05:00
def _lookup(self, path):
# fetch the possibly nested value from last_response
value = self.last_response
if path == "$body":
return value
path = path.replace(r"\.", "\1")
for step in path.split("."):
if not step:
continue
2020-05-14 16:09:24 -05:00
step = step.replace("\1", ".")
step = self._resolve(step)
if step.isdigit() and step not in value:
step = int(step)
assert isinstance(value, list)
assert len(value) > step
else:
assert step in value
value = value[step]
return value
2020-05-14 16:09:24 -05:00
def _feature_enabled(self, name):
global XPACK_FEATURES, IMPLEMENTED_FEATURES
if XPACK_FEATURES is None:
try:
xinfo = self.client.xpack.info()
XPACK_FEATURES = set(
f for f in xinfo["features"] if xinfo["features"][f]["enabled"]
)
IMPLEMENTED_FEATURES.add("xpack")
except RequestError:
XPACK_FEATURES = set()
IMPLEMENTED_FEATURES.add("no_xpack")
return name in XPACK_FEATURES
2020-05-14 16:09:24 -05:00
class InvalidActionType(Exception):
pass
2019-03-29 09:25:23 -06:00
2013-08-01 14:47:34 +02:00
YAML_DIR = environ.get(
2019-03-29 09:25:23 -06:00
"TEST_ES_YAML_DIR",
2013-08-01 14:47:34 +02:00
join(
2019-03-29 09:25:23 -06:00
dirname(__file__),
pardir,
pardir,
pardir,
"elasticsearch",
"rest-api-spec",
"src",
"main",
"resources",
"rest-api-spec",
"test",
),
2013-08-01 14:47:34 +02:00
)
2015-08-25 01:09:54 +02:00
2020-05-14 16:09:24 -05:00
YAML_TEST_SPECS = []
2013-07-31 17:51:42 +02:00
if exists(YAML_DIR):
2019-03-29 09:25:23 -06:00
# find all the test definitions in yaml files ...
2020-05-14 16:09:24 -05:00
for path, _, files in walk(YAML_DIR):
for filename in files:
2019-03-29 09:25:23 -06:00
if not filename.endswith((".yaml", ".yml")):
continue
2020-05-14 16:09:24 -05:00
filepath = join(path, filename)
with open(filepath) as f:
tests = list(yaml.load_all(f, Loader=yaml.SafeLoader))
setup_code = None
teardown_code = None
run_codes = []
for i, test in enumerate(tests):
for test_name, definition in test.items():
if test_name == "setup":
setup_code = definition
elif test_name == "teardown":
teardown_code = definition
else:
run_codes.append((i, definition))
for i, run_code in run_codes:
src = {"setup": setup_code, "run": run_code, "teardown": teardown_code}
# Pytest already replaces '.' and '_' with '/' so we do
# it ourselves so UI and 'SKIP_TESTS' match.
pytest_param_id = (
"%s[%d]" % (relpath(filepath, YAML_DIR).rpartition(".")[0], i)
).replace(".", "/")
if pytest_param_id in SKIP_TESTS:
src["skip"] = True
YAML_TEST_SPECS.append(pytest.param(src, id=pytest_param_id))
@pytest.fixture(scope="function")
def sync_runner(sync_client):
return YamlRunner(sync_client)
if not RUN_ASYNC_REST_API_TESTS:
@pytest.mark.parametrize("test_spec", YAML_TEST_SPECS)
def test_rest_api_spec(test_spec, sync_runner):
if test_spec.get("skip", False):
pytest.skip("Manually skipped in 'SKIP_TESTS'")
sync_runner.use_spec(test_spec)
sync_runner.run()