2021-08-06 12:59:39 +05:30
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
#
|
|
|
|
|
# The OpenSearch Contributors require contributions made to
|
|
|
|
|
# this file be licensed under the Apache-2.0 license or a
|
|
|
|
|
# compatible open source license.
|
|
|
|
|
#
|
|
|
|
|
# Modifications Copyright OpenSearch Contributors. See
|
|
|
|
|
# GitHub history for details.
|
|
|
|
|
#
|
2020-07-02 13:15:25 -05:00
|
|
|
# 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
|
|
|
|
2022-10-04 00:15:18 +05:30
|
|
|
|
2013-06-14 17:18:50 +02:00
|
|
|
"""
|
2020-05-14 16:09:24 -05:00
|
|
|
Dynamically generated set of TestCases based on set of yaml files describing
|
2021-08-13 15:51:50 +05:30
|
|
|
some integration tests. These files are shared among all official OpenSearch
|
2013-06-14 17:18:50 +02:00
|
|
|
clients.
|
|
|
|
|
"""
|
2021-04-22 12:08:05 -05:00
|
|
|
import io
|
2020-05-15 09:37:49 -05:00
|
|
|
import os
|
2021-01-13 14:21:04 -06:00
|
|
|
import re
|
2020-03-31 14:44:20 -05:00
|
|
|
import warnings
|
2021-04-22 12:08:05 -05:00
|
|
|
import zipfile
|
2023-11-09 10:51:20 -05:00
|
|
|
from typing import Any
|
2021-01-13 14:21:04 -06:00
|
|
|
|
2020-05-14 16:09:24 -05:00
|
|
|
import pytest
|
2021-04-22 12:08:05 -05:00
|
|
|
import urllib3
|
2021-01-13 14:21:04 -06:00
|
|
|
import yaml
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2021-09-16 14:59:29 +05:30
|
|
|
from opensearchpy import OpenSearchWarning, TransportError
|
|
|
|
|
from opensearchpy.client.utils import _base64_auth_header
|
|
|
|
|
from opensearchpy.compat import string_types
|
|
|
|
|
from opensearchpy.helpers.test import _get_version
|
2013-11-14 01:08:19 +01:00
|
|
|
|
2021-04-22 12:08:05 -05:00
|
|
|
from . import get_client
|
|
|
|
|
|
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}
|
2013-06-14 17:18:50 +02:00
|
|
|
|
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",
|
2020-03-31 14:44:20 -05:00
|
|
|
"warnings",
|
2020-05-19 13:39:14 -05:00
|
|
|
"allowed_warnings",
|
2021-01-25 08:44:39 -06:00
|
|
|
"contains",
|
2021-01-25 09:36:15 -06:00
|
|
|
"arbitrary_key",
|
2021-07-15 01:28:27 +05:30
|
|
|
"transform_and_set",
|
2020-03-09 11:51:35 -05:00
|
|
|
}
|
2014-02-03 19:10:58 +01:00
|
|
|
|
2014-06-25 15:32:35 +02:00
|
|
|
# broken YAML tests on some releases
|
|
|
|
|
SKIP_TESTS = {
|
2023-06-26 16:07:06 -07:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cat/indices/10_basic[2]",
|
2024-07-18 10:44:41 -05:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cat/nodeattrs/10_basic[1]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cat/nodes/10_basic[1]",
|
2023-06-26 16:07:06 -07:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cluster/health/10_basic[6]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cluster/health/20_request_timeout",
|
2024-07-18 10:44:41 -05:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cluster/put_settings/10_basic[2]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/cluster/put_settings/10_basic[3]",
|
2023-06-26 16:07:06 -07:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/index/90_unsigned_long[1]",
|
2024-07-18 15:24:38 -05:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/indices/put_alias/all_path_options[5]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/indices/put_alias/all_path_options[6]",
|
2024-01-19 13:36:05 -05:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/indices/stats/50_noop_update[0]",
|
2024-02-02 09:34:31 -08:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/search/340_doc_values_field[0]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/search/340_doc_values_field[1]",
|
2024-07-18 10:44:41 -05:00
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/search/aggregation/20_terms[4]",
|
|
|
|
|
"OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/tasks/list/10_basic[0]",
|
2014-06-25 15:32:35 +02:00
|
|
|
}
|
|
|
|
|
|
2020-04-03 12:52:11 -05:00
|
|
|
|
2021-08-18 00:41:42 +05:30
|
|
|
OPENSEARCH_VERSION = None
|
2020-05-15 09:37:49 -05:00
|
|
|
RUN_ASYNC_REST_API_TESTS = (
|
2023-10-24 10:57:21 -04:00
|
|
|
os.environ.get("PYTHON_CONNECTION_CLASS") == "RequestsHttpConnection"
|
2020-05-15 09:37:49 -05:00
|
|
|
)
|
2020-01-19 00:46:24 +00:00
|
|
|
|
2021-07-13 19:57:34 -05:00
|
|
|
FALSEY_VALUES = ("", None, False, 0, 0.0)
|
|
|
|
|
|
2019-03-29 09:25:23 -06:00
|
|
|
|
2020-05-14 16:09:24 -05:00
|
|
|
class YamlRunner:
|
2023-11-09 10:51:20 -05:00
|
|
|
def __init__(self, client: Any) -> None:
|
2020-05-14 16:09:24 -05:00
|
|
|
self.client = client
|
2023-11-09 10:51:20 -05:00
|
|
|
self.last_response: Any = None
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
self._run_code: Any = None
|
|
|
|
|
self._setup_code: Any = None
|
|
|
|
|
self._teardown_code: Any = None
|
|
|
|
|
self._state: Any = {}
|
2020-05-14 16:09:24 -05:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def use_spec(self, test_spec: Any) -> None:
|
2020-05-14 16:09:24 -05:00
|
|
|
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)
|
2019-03-29 09:25:23 -06:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def setup(self) -> Any:
|
2024-01-19 13:36:05 -05:00
|
|
|
"""Pull skips from individual tests to not do unnecessary setup."""
|
2023-11-09 10:51:20 -05:00
|
|
|
skip_code: Any = []
|
2021-07-13 19:57:34 -05:00
|
|
|
for action in self._run_code:
|
|
|
|
|
assert len(action) == 1
|
|
|
|
|
action_type, _ = list(action.items())[0]
|
|
|
|
|
if action_type == "skip":
|
|
|
|
|
skip_code.append(action)
|
|
|
|
|
else:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if self._setup_code or skip_code:
|
|
|
|
|
self.section("setup")
|
|
|
|
|
if skip_code:
|
|
|
|
|
self.run_code(skip_code)
|
2020-05-14 16:09:24 -05:00
|
|
|
if self._setup_code:
|
2013-09-25 21:49:23 +02:00
|
|
|
self.run_code(self._setup_code)
|
2013-07-10 13:25:09 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def teardown(self) -> Any:
|
2020-05-14 16:09:24 -05:00
|
|
|
if self._teardown_code:
|
2021-07-13 19:57:34 -05:00
|
|
|
self.section("teardown")
|
2016-07-11 11:01:56 +02:00
|
|
|
self.run_code(self._teardown_code)
|
2020-05-14 16:09:24 -05:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def opensearch_version(self) -> Any:
|
2021-08-18 00:41:42 +05:30
|
|
|
global OPENSEARCH_VERSION
|
|
|
|
|
if OPENSEARCH_VERSION is None:
|
2020-05-14 16:09:24 -05:00
|
|
|
version_string = (self.client.info())["version"]["number"]
|
|
|
|
|
if "." not in version_string:
|
|
|
|
|
return ()
|
|
|
|
|
version = version_string.strip().split(".")
|
2023-06-26 16:07:06 -07:00
|
|
|
OPENSEARCH_VERSION = tuple(int(v) if v.isdigit() else 99 for v in version)
|
2021-08-18 00:41:42 +05:30
|
|
|
return OPENSEARCH_VERSION
|
2020-05-14 16:09:24 -05:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def section(self, name: str) -> None:
|
2021-07-13 19:57:34 -05:00
|
|
|
print(("=" * 10) + " " + name + " " + ("=" * 10))
|
|
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run(self) -> Any:
|
2020-05-14 16:09:24 -05:00
|
|
|
try:
|
|
|
|
|
self.setup()
|
2021-07-13 19:57:34 -05:00
|
|
|
self.section("test")
|
2020-05-14 16:09:24 -05:00
|
|
|
self.run_code(self._run_code)
|
|
|
|
|
finally:
|
2021-07-13 19:57:34 -05:00
|
|
|
try:
|
|
|
|
|
self.teardown()
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2013-07-10 17:03:48 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_code(self, test: Any) -> Any:
|
2023-10-09 13:45:18 -04:00
|
|
|
"""Execute an instruction based on its type."""
|
2013-06-14 17:18:50 +02:00
|
|
|
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]
|
2021-07-13 19:57:34 -05:00
|
|
|
print(action_type, action)
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2019-03-29 09:25:23 -06:00
|
|
|
if hasattr(self, "run_" + action_type):
|
|
|
|
|
getattr(self, "run_" + action_type)(action)
|
2013-06-14 17:18:50 +02:00
|
|
|
else:
|
2024-07-20 23:19:20 +03:00
|
|
|
raise RuntimeError(f"Invalid action type {action_type!r}")
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_do(self, action: Any) -> Any:
|
2016-07-12 22:45:27 +02:00
|
|
|
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", ())
|
2020-05-19 13:39:14 -05:00
|
|
|
allowed_warnings = action.pop("allowed_warnings", ())
|
2020-05-14 16:09:24 -05:00
|
|
|
assert len(action) == 1
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2021-04-22 12:08:05 -05:00
|
|
|
# Remove the x_pack_rest_user authentication
|
|
|
|
|
# if it's given via headers. We're already authenticated
|
|
|
|
|
# via the 'elastic' user.
|
|
|
|
|
if (
|
|
|
|
|
headers
|
|
|
|
|
and headers.get("Authorization", None)
|
|
|
|
|
== "Basic eF9wYWNrX3Jlc3RfdXNlcjp4LXBhY2stdGVzdC1wYXNzd29yZA=="
|
|
|
|
|
):
|
|
|
|
|
headers.pop("Authorization")
|
|
|
|
|
|
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
|
2013-06-14 17:18:50 +02:00
|
|
|
|
|
|
|
|
# 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)
|
2013-06-14 17:18:50 +02:00
|
|
|
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])
|
|
|
|
|
|
2021-08-13 15:51:50 +05:30
|
|
|
warnings.simplefilter("always", category=OpenSearchWarning)
|
2020-03-31 14:44:20 -05:00
|
|
|
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(
|
2024-07-20 23:19:20 +03:00
|
|
|
f"Failed to catch {catch!r} in {self.last_response!r}."
|
2020-03-31 14:44:20 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Filter out warnings raised by other components.
|
|
|
|
|
caught_warnings = [
|
2023-11-09 10:51:20 -05:00
|
|
|
str(w.message) # type: ignore
|
2020-03-31 14:44:20 -05:00
|
|
|
for w in caught_warnings
|
2021-08-13 15:51:50 +05:30
|
|
|
if w.category == OpenSearchWarning
|
2020-05-19 13:39:14 -05:00
|
|
|
and str(w.message) not in allowed_warnings
|
2020-03-31 14:44:20 -05:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Sorting removes the issue with order raised. We only care about
|
|
|
|
|
# if all warnings are raised in the single API call.
|
2023-11-09 10:51:20 -05:00
|
|
|
if warn and sorted(warn) != sorted(caught_warnings): # type: ignore
|
2020-03-31 14:44:20 -05:00
|
|
|
raise AssertionError(
|
|
|
|
|
"Expected warnings not equal to actual warnings: expected=%r actual=%r"
|
|
|
|
|
% (warn, caught_warnings)
|
|
|
|
|
)
|
2013-07-10 13:40:33 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_catch(self, catch: Any, exception: Any) -> None:
|
2020-05-14 16:09:24 -05:00
|
|
|
if catch == "param":
|
|
|
|
|
assert isinstance(exception, TypeError)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
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)),
|
2024-07-20 23:19:20 +03:00
|
|
|
f"{catch} not in {exception.info!r}",
|
2020-05-14 16:09:24 -05:00
|
|
|
) is not None
|
|
|
|
|
self.last_response = exception.info
|
2015-01-12 14:57:52 +01:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_skip(self, skip: Any) -> Any:
|
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
|
2024-07-20 23:19:20 +03:00
|
|
|
pytest.skip(f"feature '{feature}' is not supported")
|
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,)
|
2021-08-18 00:41:42 +05:30
|
|
|
if min_version <= (self.opensearch_version()) <= max_version:
|
2020-05-14 16:09:24 -05:00
|
|
|
pytest.skip(reason)
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_gt(self, action: Any) -> None:
|
2013-07-10 17:03:48 +02:00
|
|
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_gte(self, action: Any) -> None:
|
2014-03-28 17:42:51 +01:00
|
|
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_lt(self, action: Any) -> None:
|
2013-07-10 17:03:48 +02:00
|
|
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_lte(self, action: Any) -> None:
|
2014-03-28 17:42:51 +01:00
|
|
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_set(self, action: Any) -> None:
|
2013-07-10 16:43:20 +02:00
|
|
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_is_false(self, action: Any) -> None:
|
2013-07-11 02:31:40 +02:00
|
|
|
try:
|
|
|
|
|
value = self._lookup(action)
|
|
|
|
|
except AssertionError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2021-07-13 19:57:34 -05:00
|
|
|
assert value in FALSEY_VALUES
|
2013-07-11 02:31:40 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_is_true(self, action: Any) -> None:
|
2013-07-10 17:03:48 +02:00
|
|
|
value = self._lookup(action)
|
2021-07-13 19:57:34 -05:00
|
|
|
assert value not in FALSEY_VALUES
|
2013-07-10 16:43:20 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_length(self, action: Any) -> None:
|
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
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_match(self, action: Any) -> None:
|
2013-07-10 17:03:48 +02:00
|
|
|
for path, expected in action.items():
|
|
|
|
|
value = self._lookup(path)
|
|
|
|
|
expected = self._resolve(expected)
|
2014-02-03 19:30:20 +01:00
|
|
|
|
2019-03-29 09:25:23 -06:00
|
|
|
if (
|
2023-11-09 10:51:20 -05:00
|
|
|
isinstance(expected, str)
|
2024-03-16 04:58:10 -07:00
|
|
|
and expected.strip().startswith("/")
|
|
|
|
|
and expected.strip().endswith("/")
|
2019-03-29 09:25:23 -06:00
|
|
|
):
|
2024-03-16 04:58:10 -07:00
|
|
|
expected = re.compile(expected.strip()[1:-1], re.VERBOSE | re.MULTILINE)
|
2024-07-20 23:19:20 +03:00
|
|
|
assert expected.search(value), f"{value!r} does not match {expected!r}"
|
2014-02-03 19:30:20 +01:00
|
|
|
else:
|
2021-07-13 19:57:34 -05:00
|
|
|
self._assert_match_equals(value, expected)
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_contains(self, action: Any) -> None:
|
2021-01-25 08:44:39 -06:00
|
|
|
for path, expected in action.items():
|
|
|
|
|
value = self._lookup(path) # list[dict[str,str]] is returned
|
|
|
|
|
expected = self._resolve(expected) # dict[str, str]
|
|
|
|
|
|
|
|
|
|
if expected not in value:
|
2024-07-20 23:19:20 +03:00
|
|
|
raise AssertionError(f"{expected} is not contained by {value}")
|
2021-01-25 08:44:39 -06:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def run_transform_and_set(self, action: Any) -> None:
|
2021-07-15 01:28:27 +05:30
|
|
|
for key, value in action.items():
|
|
|
|
|
# Convert #base64EncodeCredentials(id,api_key) to ["id", "api_key"]
|
|
|
|
|
if "#base64EncodeCredentials" in value:
|
|
|
|
|
value = value.replace("#base64EncodeCredentials", "")
|
|
|
|
|
value = value.replace("(", "").replace(")", "").split(",")
|
|
|
|
|
self._state[key] = _base64_auth_header(
|
|
|
|
|
(self._lookup(value[0]), self._lookup(value[1]))
|
|
|
|
|
)
|
|
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def _resolve(self, value: Any) -> Any:
|
2020-05-14 16:09:24 -05:00
|
|
|
# resolve variables
|
2021-07-15 01:28:27 +05:30
|
|
|
if isinstance(value, string_types) and "$" in value:
|
|
|
|
|
for k, v in self._state.items():
|
2021-07-13 19:57:34 -05:00
|
|
|
for key_replace in ("${" + k + "}", "$" + k):
|
|
|
|
|
if value == key_replace:
|
|
|
|
|
value = v
|
|
|
|
|
break
|
|
|
|
|
# We only do the in-string replacement if using ${...}
|
|
|
|
|
elif (
|
|
|
|
|
key_replace.startswith("${")
|
|
|
|
|
and isinstance(value, string_types)
|
|
|
|
|
and key_replace in value
|
|
|
|
|
):
|
|
|
|
|
value = value.replace(key_replace, v)
|
|
|
|
|
break
|
2021-07-15 01:28:27 +05:30
|
|
|
|
2024-03-16 04:58:10 -07:00
|
|
|
if isinstance(value, dict):
|
2024-07-20 23:19:20 +03:00
|
|
|
value = {k: self._resolve(v) for (k, v) in value.items()}
|
2020-05-14 16:09:24 -05:00
|
|
|
elif isinstance(value, list):
|
|
|
|
|
value = list(map(self._resolve, value))
|
|
|
|
|
return value
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def _lookup(self, path: str) -> Any:
|
2020-05-14 16:09:24 -05:00
|
|
|
# fetch the possibly nested value from last_response
|
2023-11-09 10:51:20 -05:00
|
|
|
value: Any = self.last_response
|
2020-05-14 16:09:24 -05:00
|
|
|
if path == "$body":
|
|
|
|
|
return value
|
|
|
|
|
path = path.replace(r"\.", "\1")
|
2023-11-09 10:51:20 -05:00
|
|
|
step: Any
|
2020-05-14 16:09:24 -05:00
|
|
|
for step in path.split("."):
|
|
|
|
|
if not step:
|
2013-09-25 21:49:23 +02:00
|
|
|
continue
|
2020-05-14 16:09:24 -05:00
|
|
|
step = step.replace("\1", ".")
|
|
|
|
|
step = self._resolve(step)
|
2021-07-13 19:57:34 -05:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
isinstance(step, string_types)
|
|
|
|
|
and step.isdigit()
|
|
|
|
|
and isinstance(value, list)
|
|
|
|
|
):
|
2020-05-14 16:09:24 -05:00
|
|
|
step = int(step)
|
|
|
|
|
assert isinstance(value, list)
|
|
|
|
|
assert len(value) > step
|
2021-01-25 09:36:15 -06:00
|
|
|
elif step == "_arbitrary_key_":
|
|
|
|
|
return list(value.keys())[0]
|
2020-05-14 16:09:24 -05:00
|
|
|
else:
|
|
|
|
|
assert step in value
|
|
|
|
|
value = value[step]
|
|
|
|
|
return value
|
|
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def _feature_enabled(self, name: str) -> Any:
|
2021-08-06 18:36:38 +05:30
|
|
|
return False
|
2013-09-25 21:49:23 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
def _assert_match_equals(self, a: Any, b: Any) -> None:
|
2021-07-13 19:57:34 -05:00
|
|
|
# Handle for large floating points with 'E'
|
|
|
|
|
if isinstance(b, string_types) and isinstance(a, float) and "e" in repr(a):
|
|
|
|
|
a = repr(a).replace("e+", "E")
|
|
|
|
|
|
2024-07-20 23:19:20 +03:00
|
|
|
assert a == b, f"{a!r} does not match {b!r}"
|
2021-07-13 19:57:34 -05:00
|
|
|
|
2013-06-14 17:18:50 +02:00
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
@pytest.fixture(scope="function") # type: ignore
|
|
|
|
|
def sync_runner(sync_client: Any) -> Any:
|
2021-04-22 12:08:05 -05:00
|
|
|
return YamlRunner(sync_client)
|
2013-07-10 13:25:09 +02:00
|
|
|
|
2015-08-25 01:09:54 +02:00
|
|
|
|
2020-05-14 16:09:24 -05:00
|
|
|
YAML_TEST_SPECS = []
|
|
|
|
|
|
2023-11-21 13:04:39 -05:00
|
|
|
client = get_client()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_rest_api_tests() -> None:
|
2024-01-19 13:36:05 -05:00
|
|
|
"""Try loading the REST API test specs from OpenSearch core."""
|
2023-11-21 13:04:39 -05:00
|
|
|
try:
|
|
|
|
|
# Construct the HTTP and OpenSearch client
|
|
|
|
|
http = urllib3.PoolManager(retries=10)
|
2020-05-14 16:09:24 -05:00
|
|
|
|
2023-11-21 13:04:39 -05:00
|
|
|
package_url = (
|
|
|
|
|
"https://github.com/opensearch-project/OpenSearch/archive/main.zip"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Download the zip and start reading YAML from the files in memory
|
|
|
|
|
package_zip = zipfile.ZipFile(io.BytesIO(http.request("GET", package_url).data))
|
|
|
|
|
for yaml_file in package_zip.namelist():
|
|
|
|
|
if not re.match(
|
|
|
|
|
r"^OpenSearch-main/rest-api-spec/src/main/resources/rest-api-spec/test/.*\.ya?ml$",
|
|
|
|
|
yaml_file,
|
|
|
|
|
):
|
|
|
|
|
continue
|
|
|
|
|
yaml_tests = list(yaml.safe_load_all(package_zip.read(yaml_file)))
|
|
|
|
|
|
|
|
|
|
# Each file may have a "test" named 'setup' or 'teardown',
|
|
|
|
|
# these sets of steps should be run at the beginning and end
|
|
|
|
|
# of every other test within the file so we do one pass to capture those.
|
|
|
|
|
setup_steps = teardown_steps = None
|
|
|
|
|
test_numbers_and_steps = []
|
|
|
|
|
test_number = 0
|
|
|
|
|
|
|
|
|
|
for yaml_test in yaml_tests:
|
|
|
|
|
test_name, test_step = yaml_test.popitem()
|
|
|
|
|
if test_name == "setup":
|
|
|
|
|
setup_steps = test_step
|
|
|
|
|
elif test_name == "teardown":
|
|
|
|
|
teardown_steps = test_step
|
|
|
|
|
else:
|
|
|
|
|
test_numbers_and_steps.append((test_number, test_step))
|
|
|
|
|
test_number += 1
|
|
|
|
|
|
|
|
|
|
# Now we combine setup, teardown, and test_steps into
|
|
|
|
|
# a set of pytest.param() instances
|
|
|
|
|
for test_number, test_step in test_numbers_and_steps:
|
|
|
|
|
# Build the id from the name of the YAML file and
|
|
|
|
|
# the number within that file. Most important step
|
|
|
|
|
# is to remove most of the file path prefixes and
|
|
|
|
|
# the .yml suffix.
|
|
|
|
|
pytest_test_name = yaml_file.rpartition(".")[0].replace(".", "/")
|
|
|
|
|
for prefix in ("rest-api-spec/", "test/", "oss/"):
|
|
|
|
|
if pytest_test_name.startswith(prefix):
|
|
|
|
|
pytest_test_name = pytest_test_name[len(prefix) :]
|
2024-07-20 23:19:20 +03:00
|
|
|
pytest_param_id = f"{pytest_test_name}[{test_number}]"
|
2023-11-21 13:04:39 -05:00
|
|
|
|
|
|
|
|
pytest_param = {
|
|
|
|
|
"setup": setup_steps,
|
|
|
|
|
"run": test_step,
|
|
|
|
|
"teardown": teardown_steps,
|
|
|
|
|
}
|
|
|
|
|
# Skip either 'test_name' or 'test_name[x]'
|
|
|
|
|
if pytest_test_name in SKIP_TESTS or pytest_param_id in SKIP_TESTS:
|
|
|
|
|
pytest_param["skip"] = True
|
|
|
|
|
|
|
|
|
|
YAML_TEST_SPECS.append(pytest.param(pytest_param, id=pytest_param_id))
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2024-07-20 23:19:20 +03:00
|
|
|
warnings.warn(f"Could not load REST API tests: {str(e)}")
|
2023-11-21 13:04:39 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
load_rest_api_tests()
|
2020-05-14 16:09:24 -05:00
|
|
|
|
2020-05-27 16:42:40 -05:00
|
|
|
if not RUN_ASYNC_REST_API_TESTS:
|
|
|
|
|
|
2023-11-09 10:51:20 -05:00
|
|
|
@pytest.mark.parametrize("test_spec", YAML_TEST_SPECS) # type: ignore
|
|
|
|
|
def test_rest_api_spec(test_spec: Any, sync_runner: Any) -> None:
|
2020-05-27 16:42:40 -05:00
|
|
|
if test_spec.get("skip", False):
|
|
|
|
|
pytest.skip("Manually skipped in 'SKIP_TESTS'")
|
|
|
|
|
sync_runner.use_spec(test_spec)
|
|
|
|
|
sync_runner.run()
|