All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.test.hamcrest.ElasticsearchAssertions Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */
package org.elasticsearch.test.hamcrest;

import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TotalHits;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.action.ActionFuture;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.RequestBuilder;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequestBuilder;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesResponse;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.search.ClearScrollResponse;
import org.elasticsearch.action.search.SearchPhaseExecutionException;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.ShardSearchFailure;
import org.elasticsearch.action.support.DefaultShardOperationFailedException;
import org.elasticsearch.action.support.broadcast.BaseBroadcastResponse;
import org.elasticsearch.action.support.master.IsAcknowledgedSupplier;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.cluster.block.ClusterBlock;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.IndexTemplateMetadata;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.NotEqualMessageBuilder;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

import static org.apache.lucene.tests.util.LuceneTestCase.expectThrows;
import static org.apache.lucene.tests.util.LuceneTestCase.expectThrowsAnyOf;
import static org.elasticsearch.test.LambdaMatchers.transformedArrayItemsMatch;
import static org.elasticsearch.test.LambdaMatchers.transformedItemsMatch;
import static org.elasticsearch.test.LambdaMatchers.transformedMatch;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class ElasticsearchAssertions {

    public static void assertAcked(RequestBuilder builder) {
        assertAcked(builder, TimeValue.timeValueSeconds(30));
    }

    public static void assertAcked(ActionFuture future) {
        assertAcked(future.actionGet());
    }

    public static void assertAcked(RequestBuilder builder, TimeValue timeValue) {
        assertAcked(builder.get(timeValue));
    }

    public static void assertNoTimeout(ClusterHealthRequestBuilder requestBuilder) {
        assertNoTimeout(requestBuilder.get());
    }

    public static void assertNoTimeout(ClusterHealthResponse response) {
        assertThat("ClusterHealthResponse has timed out - returned: [" + response + "]", response.isTimedOut(), is(false));
    }

    public static void assertAcked(IsAcknowledgedSupplier response) {
        assertThat(response.getClass().getSimpleName() + " failed - not acked", response.isAcknowledged(), equalTo(true));
    }

    /**
     * Assert that an index creation was fully acknowledged, meaning that both the index creation cluster
     * state update was successful and that the requisite number of shard copies were started before returning.
     */
    public static void assertAcked(CreateIndexResponse response) {
        assertThat(response.getClass().getSimpleName() + " failed - not acked", response.isAcknowledged(), is(true));
        assertThat(
            response.getClass().getSimpleName() + " failed - index creation acked but not all shards were started",
            response.isShardsAcknowledged(),
            is(true)
        );
    }

    /**
     * Executes the request and fails if the request has not been blocked.
     *
     * @param builder the request builder
     */
    public static void assertBlocked(RequestBuilder builder) {
        assertBlocked(builder, (ClusterBlock) null);
    }

    /**
     * Checks that all shard requests of a replicated broadcast request failed due to a cluster block
     *
     * @param replicatedBroadcastResponse the response that should only contain failed shard responses
     *
     * */
    public static void assertBlocked(BaseBroadcastResponse replicatedBroadcastResponse) {
        assertThat(
            "all shard requests should have failed",
            replicatedBroadcastResponse.getFailedShards(),
            equalTo(replicatedBroadcastResponse.getTotalShards())
        );
        for (DefaultShardOperationFailedException exception : replicatedBroadcastResponse.getShardFailures()) {
            ClusterBlockException clusterBlockException = (ClusterBlockException) ExceptionsHelper.unwrap(
                exception.getCause(),
                ClusterBlockException.class
            );
            assertNotNull(
                "expected the cause of failure to be a ClusterBlockException but got " + exception.getCause().getMessage(),
                clusterBlockException
            );
            assertThat(clusterBlockException.blocks(), not(empty()));

            RestStatus status = checkRetryableBlock(clusterBlockException.blocks()) ? RestStatus.TOO_MANY_REQUESTS : RestStatus.FORBIDDEN;
            assertThat(clusterBlockException.status(), equalTo(status));
        }
    }

    /**
     * Executes the request and fails if the request has not been blocked by a specific {@link ClusterBlock}.
     *
     * @param builder the request builder
     * @param expectedBlockId the expected block id
     */
    public static void assertBlocked(final RequestBuilder builder, @Nullable final Integer expectedBlockId) {
        assertBlocked(expectedBlockId, ESTestCase.expectThrows(ClusterBlockException.class, builder));
    }

    /**
     * Checks that the given exception is a {@link ClusterBlockException}; if the given block ID is not {@code null} then the given
     * exception must match that ID.
     */
    public static void assertBlocked(@Nullable final Integer expectedBlockId, Exception exception) {
        final var e = ESTestCase.asInstanceOf(ClusterBlockException.class, exception);
        assertThat(e.blocks(), not(empty()));
        assertThat(e.status(), equalTo(checkRetryableBlock(e.blocks()) ? RestStatus.TOO_MANY_REQUESTS : RestStatus.FORBIDDEN));

        if (expectedBlockId != null) {
            assertThat(
                "Request should have been blocked by [" + expectedBlockId + "] instead of " + e.blocks(),
                e.blocks(),
                hasItem(transformedMatch(ClusterBlock::id, equalTo(expectedBlockId)))
            );
        }
    }

    /**
     * Executes the request and fails if the request has not been blocked by a specific {@link ClusterBlock}.
     *
     * @param builder the request builder
     * @param expectedBlock the expected block
     */
    public static void assertBlocked(final RequestBuilder builder, @Nullable final ClusterBlock expectedBlock) {
        assertBlocked(builder, expectedBlock != null ? expectedBlock.id() : null);
    }

    private static boolean checkRetryableBlock(Set clusterBlocks) {
        // check only retryable blocks exist in the set
        return clusterBlocks.stream().allMatch(clusterBlock -> clusterBlock.id() == IndexMetadata.INDEX_READ_ONLY_ALLOW_DELETE_BLOCK.id());
    }

    public static String formatShardStatus(BaseBroadcastResponse response) {
        StringBuilder msg = new StringBuilder();
        msg.append(" Total shards: ")
            .append(response.getTotalShards())
            .append(" Successful shards: ")
            .append(response.getSuccessfulShards())
            .append(" & ")
            .append(response.getFailedShards())
            .append(" shard failures:");
        for (DefaultShardOperationFailedException failure : response.getShardFailures()) {
            msg.append("\n ").append(failure);
        }
        return msg.toString();
    }

    public static String formatShardStatus(SearchResponse response) {
        StringBuilder msg = new StringBuilder();
        msg.append(" Total shards: ")
            .append(response.getTotalShards())
            .append(" Successful shards: ")
            .append(response.getSuccessfulShards())
            .append(" & ")
            .append(response.getFailedShards())
            .append(" shard failures:");
        for (ShardSearchFailure failure : response.getShardFailures()) {
            msg.append("\n ").append(failure);
        }
        return msg.toString();
    }

    public static void assertNoSearchHits(SearchRequestBuilder searchRequestBuilder) {
        assertResponse(searchRequestBuilder, ElasticsearchAssertions::assertNoSearchHits);
    }

    public static void assertNoSearchHits(SearchResponse searchResponse) {
        assertThat(searchResponse.getHits().getHits(), emptyArray());
    }

    public static void assertSearchHits(SearchRequestBuilder searchRequestBuilder, String... ids) {
        assertResponse(searchRequestBuilder, res -> assertSearchHits(res, ids));
    }

    public static void assertSearchHits(SearchResponse searchResponse, String... ids) {
        assertThat(
            "Incorrect SearchHit ids. " + formatShardStatus(searchResponse),
            searchResponse.getHits(),
            transformedItemsMatch(SearchHit::getId, containsInAnyOrder(ids))
        );
    }

    public static void assertSearchHitsWithoutFailures(SearchRequestBuilder requestBuilder, String... ids) {
        assertResponse(requestBuilder, res -> {
            assertNoFailures(res);
            assertHitCount(res, ids.length);
            assertSearchHits(res, ids);
        });
    }

    public static void assertSortValues(SearchRequestBuilder searchRequestBuilder, Object[]... sortValues) {
        assertNoFailuresAndResponse(searchRequestBuilder, res -> {
            SearchHit[] hits = res.getHits().getHits();
            assertEquals(sortValues.length, hits.length);
            for (int i = 0; i < sortValues.length; ++i) {
                final Object[] hitsSortValues = hits[i].getSortValues();
                assertArrayEquals("Offset " + i + ", id " + hits[i].getId(), sortValues[i], hitsSortValues);
            }
        });
    }

    public static void assertOrderedSearchHits(SearchRequestBuilder searchRequestBuilder, String... ids) {
        assertResponse(searchRequestBuilder, res -> assertOrderedSearchHits(res, ids));
    }

    public static void assertOrderedSearchHits(SearchResponse searchResponse, String... ids) {
        assertThat(
            "Incorrect SearchHit ids. " + formatShardStatus(searchResponse),
            searchResponse.getHits().getHits(),
            transformedArrayItemsMatch(SearchHit::getId, arrayContaining(ids))
        );
    }

    public static void assertHitCount(SearchRequestBuilder searchRequestBuilder, long expectedHitCount) {
        assertResponse(searchRequestBuilder, res -> assertHitCount(res, expectedHitCount));
    }

    public static void assertHitCount(ActionFuture responseFuture, long expectedHitCount) {
        try {
            assertResponse(responseFuture, res -> assertHitCount(res, expectedHitCount));
        } catch (ExecutionException | InterruptedException ex) {
            throw new AssertionError(ex);
        }
    }

    public static void assertHitCount(SearchResponse countResponse, long expectedHitCount) {
        final TotalHits totalHits = countResponse.getHits().getTotalHits();
        if (totalHits.relation != TotalHits.Relation.EQUAL_TO || totalHits.value != expectedHitCount) {
            fail("Count is " + totalHits + " but " + expectedHitCount + " was expected. " + formatShardStatus(countResponse));
        }
    }

    public static void assertHitCountAndNoFailures(SearchRequestBuilder searchRequestBuilder, long expectedHitCount) {
        assertNoFailuresAndResponse(searchRequestBuilder, response -> assertHitCount(response, expectedHitCount));
    }

    public static void assertExists(GetResponse response) {
        String message = String.format(Locale.ROOT, "Expected %s/%s to exist, but does not", response.getIndex(), response.getId());
        assertThat(message, response.isExists(), is(true));
    }

    public static void assertFirstHit(SearchResponse searchResponse, Matcher matcher) {
        assertSearchHit(searchResponse, 1, matcher);
    }

    public static void assertSecondHit(SearchResponse searchResponse, Matcher matcher) {
        assertSearchHit(searchResponse, 2, matcher);
    }

    public static void assertThirdHit(SearchResponse searchResponse, Matcher matcher) {
        assertSearchHit(searchResponse, 3, matcher);
    }

    public static void assertFourthHit(SearchResponse searchResponse, Matcher matcher) {
        assertSearchHit(searchResponse, 4, matcher);
    }

    public static void assertSearchHit(SearchResponse searchResponse, int number, Matcher matcher) {
        assertThat("SearchHit number must be greater than 0", number, greaterThan(0));
        assertThat(searchResponse.getHits().getTotalHits().value, greaterThanOrEqualTo((long) number));
        assertThat(searchResponse.getHits().getAt(number - 1), matcher);
    }

    public static void assertNoFailures(RequestBuilder searchRequestBuilder) {
        assertNoFailuresAndResponse(searchRequestBuilder, r -> {});
    }

    public static void assertNoFailuresAndResponse(
        RequestBuilder searchRequestBuilder,
        Consumer consumer
    ) {
        assertResponse(searchRequestBuilder, res -> {
            assertNoFailures(res);
            consumer.accept(res);
        });
    }

    public static void assertNoFailuresAndResponse(ActionFuture responseFuture, Consumer consumer)
        throws ExecutionException, InterruptedException {
        var res = responseFuture.get();
        try {
            assertNoFailures(res);
            consumer.accept(res);
        } finally {
            res.decRef();
        }
    }

    public static  void assertResponse(
        RequestBuilder searchRequestBuilder,
        Consumer consumer
    ) {
        var res = searchRequestBuilder.get();
        try {
            consumer.accept(res);
        } finally {
            res.decRef();
        }
    }

    /**
     * A helper to enable the testing of scroll requests with ref-counting.
     *
     * @param keepAlive             The TTL for the scroll context.
     * @param searchRequestBuilder  The initial search request.
     * @param expectedTotalHitCount The number of hits that are expected to be retrieved.
     * @param responseConsumer      (respNum, response) -> {your assertions here}.
     *                              respNum starts at 1, which contains the resp from the initial request.
     */
    public static void assertScrollResponsesAndHitCount(
        Client client,
        TimeValue keepAlive,
        SearchRequestBuilder searchRequestBuilder,
        int expectedTotalHitCount,
        BiConsumer responseConsumer
    ) {
        searchRequestBuilder.setScroll(keepAlive);
        List responses = new ArrayList<>();
        var scrollResponse = searchRequestBuilder.get();
        responses.add(scrollResponse);
        int retrievedDocsCount = 0;
        try {
            assertThat(scrollResponse.getHits().getTotalHits().value, equalTo((long) expectedTotalHitCount));
            retrievedDocsCount += scrollResponse.getHits().getHits().length;
            responseConsumer.accept(responses.size(), scrollResponse);
            while (scrollResponse.getHits().getHits().length > 0) {
                scrollResponse = client.prepareSearchScroll(scrollResponse.getScrollId()).setScroll(keepAlive).get();
                responses.add(scrollResponse);
                assertThat(scrollResponse.getHits().getTotalHits().value, equalTo((long) expectedTotalHitCount));
                retrievedDocsCount += scrollResponse.getHits().getHits().length;
                responseConsumer.accept(responses.size(), scrollResponse);
            }
        } finally {
            ClearScrollResponse clearResponse = client.prepareClearScroll().setScrollIds(Arrays.asList(scrollResponse.getScrollId())).get();
            responses.forEach(SearchResponse::decRef);
            assertThat(clearResponse.isSucceeded(), Matchers.equalTo(true));
        }
        assertThat(retrievedDocsCount, equalTo(expectedTotalHitCount));
    }

    public static  void assertResponse(ActionFuture responseFuture, Consumer consumer)
        throws ExecutionException, InterruptedException {
        var res = responseFuture.get();
        try {
            consumer.accept(res);
        } finally {
            res.decRef();
        }
    }

    public static void assertCheckedResponse(
        RequestBuilder searchRequestBuilder,
        CheckedConsumer consumer
    ) throws IOException {
        var res = searchRequestBuilder.get();
        try {
            consumer.accept(res);
        } finally {
            res.decRef();
        }
    }

    public static void assertCheckedResponse(
        ActionFuture responseFuture,
        CheckedConsumer consumer
    ) throws IOException, ExecutionException, InterruptedException {
        var res = responseFuture.get();
        try {
            consumer.accept(res);
        } finally {
            res.decRef();
        }
    }

    public static void assertNoFailures(SearchResponse searchResponse) {
        assertThat(
            "Unexpected ShardFailures: " + Arrays.toString(searchResponse.getShardFailures()),
            searchResponse.getShardFailures(),
            emptyArray()
        );
    }

    public static void assertFailures(SearchResponse searchResponse) {
        assertThat("Expected at least one shard failure, got none", searchResponse.getShardFailures(), not(emptyArray()));
    }

    public static void assertNoFailures(BulkResponse response) {
        assertThat("Unexpected ShardFailures: " + response.buildFailureMessage(), response.hasFailures(), is(false));
    }

    public static void assertFailures(SearchRequestBuilder searchRequestBuilder, RestStatus restStatus, Matcher reasonMatcher) {
        assertFailures(searchRequestBuilder, Set.of(restStatus), reasonMatcher);
    }

    public static void assertFailures(
        SearchRequestBuilder searchRequestBuilder,
        Set restStatuses,
        Matcher reasonMatcher
    ) {
        // when the number for shards is randomized and we expect failures
        // we can either run into partial or total failures depending on the current number of shards
        try {
            assertResponse(searchRequestBuilder, response -> {
                assertThat("Expected shard failures, got none", response.getShardFailures(), not(emptyArray()));
                for (ShardSearchFailure shardSearchFailure : response.getShardFailures()) {
                    assertThat(restStatuses, hasItem(shardSearchFailure.status()));
                    assertThat(shardSearchFailure.reason(), reasonMatcher);
                }
            });
        } catch (SearchPhaseExecutionException e) {
            assertThat(restStatuses, hasItem(e.status()));
            assertThat(e.toString(), reasonMatcher);
            for (ShardSearchFailure shardSearchFailure : e.shardFailures()) {
                assertThat(restStatuses, hasItem(shardSearchFailure.status()));
                assertThat(shardSearchFailure.reason(), reasonMatcher);
            }
        } catch (Exception e) {
            fail("SearchPhaseExecutionException expected but got " + e.getClass());
        }
    }

    public static void assertFailures(SearchRequestBuilder searchRequestBuilder, RestStatus restStatus) {
        assertFailures(searchRequestBuilder, restStatus, containsString(""));
    }

    public static void assertNoFailures(BaseBroadcastResponse response) {
        if (response.getFailedShards() != 0) {
            final AssertionError assertionError = new AssertionError("[" + response.getFailedShards() + "] shard failures");

            for (DefaultShardOperationFailedException shardFailure : response.getShardFailures()) {
                assertionError.addSuppressed(new ElasticsearchException(shardFailure.toString(), shardFailure.getCause()));
            }

            throw assertionError;
        }
    }

    public static void assertAllSuccessful(BaseBroadcastResponse response) {
        assertNoFailures(response);
        assertThat("Expected all shards successful", response.getSuccessfulShards(), equalTo(response.getTotalShards()));
    }

    public static void assertAllSuccessful(SearchResponse response) {
        assertNoFailures(response);
        assertThat("Expected all shards successful", response.getSuccessfulShards(), equalTo(response.getTotalShards()));
    }

    public static void assertHighlight(SearchResponse resp, int hit, String field, int fragment, Matcher matcher) {
        assertHighlight(resp, hit, field, fragment, greaterThan(fragment), matcher);
    }

    public static void assertHighlight(
        SearchRequestBuilder searchRequestBuilder,
        int hit,
        String field,
        int fragment,
        int totalFragments,
        Matcher matcher
    ) {
        assertResponse(searchRequestBuilder, response -> assertHighlight(response, hit, field, fragment, equalTo(totalFragments), matcher));
    }

    public static void assertHighlight(
        ActionFuture responseFuture,
        int hit,
        String field,
        int fragment,
        int totalFragments,
        Matcher matcher
    ) throws ExecutionException, InterruptedException {
        assertResponse(responseFuture, response -> assertHighlight(response, hit, field, fragment, equalTo(totalFragments), matcher));
    }

    public static void assertHighlight(
        SearchResponse resp,
        int hit,
        String field,
        int fragment,
        int totalFragments,
        Matcher matcher
    ) {
        assertHighlight(resp, hit, field, fragment, equalTo(totalFragments), matcher);
    }

    public static void assertHighlight(SearchHit hit, String field, int fragment, Matcher matcher) {
        assertHighlight(hit, field, fragment, greaterThan(fragment), matcher);
    }

    public static void assertHighlight(SearchHit hit, String field, int fragment, int totalFragments, Matcher matcher) {
        assertHighlight(hit, field, fragment, equalTo(totalFragments), matcher);
    }

    private static void assertHighlight(
        SearchResponse resp,
        int hit,
        String field,
        int fragment,
        Matcher fragmentsMatcher,
        Matcher matcher
    ) {
        assertNoFailures(resp);
        assertThat("not enough hits", resp.getHits().getHits(), arrayWithSize(greaterThan(hit)));
        assertHighlight(resp.getHits().getHits()[hit], field, fragment, fragmentsMatcher, matcher);
    }

    private static void assertHighlight(
        SearchHit hit,
        String field,
        int fragment,
        Matcher fragmentsMatcher,
        Matcher matcher
    ) {
        assertThat(hit.getHighlightFields(), hasKey(field));
        assertThat(hit.getHighlightFields().get(field).fragments(), arrayWithSize(fragmentsMatcher));
        assertThat(hit.getHighlightFields().get(field).fragments()[fragment].string(), matcher);
    }

    public static void assertNotHighlighted(SearchRequestBuilder searchRequestBuilder, int hit, String field) {
        assertResponse(searchRequestBuilder, response -> assertNotHighlighted(response, hit, field));
    }

    public static void assertNotHighlighted(SearchResponse resp, int hit, String field) {
        assertNoFailures(resp);
        assertThat("not enough hits", resp.getHits().getHits(), arrayWithSize(greaterThan(hit)));
        assertThat(resp.getHits().getHits()[hit].getHighlightFields(), not(hasKey(field)));
    }

    public static void assertSuggestionSize(Suggest searchSuggest, int entry, int size, String key) {
        assertThat(searchSuggest, notNullValue());
        String msg = "Suggest result: " + searchSuggest.toString();
        assertThat(msg, searchSuggest.size(), greaterThanOrEqualTo(1));
        assertThat(msg, searchSuggest.getSuggestion(key).getName(), equalTo(key));
        assertThat(msg, searchSuggest.getSuggestion(key).getEntries(), hasSize(greaterThanOrEqualTo(entry)));
        assertThat(msg, searchSuggest.getSuggestion(key).getEntries().get(entry).getOptions(), hasSize(size));
    }

    public static void assertSuggestionPhraseCollateMatchExists(Suggest searchSuggest, String key, int numberOfPhraseExists) {
        int counter = 0;
        assertThat(searchSuggest, notNullValue());
        String msg = "Suggest result: " + searchSuggest;
        assertThat(msg, searchSuggest.size(), greaterThanOrEqualTo(1));
        assertThat(msg, searchSuggest.getSuggestion(key).getName(), equalTo(key));

        for (Suggest.Suggestion.Entry.Option option : searchSuggest.getSuggestion(key).getEntries().get(0).getOptions()) {
            if (option.collateMatch()) {
                counter++;
            }
        }

        assertThat(counter, equalTo(numberOfPhraseExists));
    }

    public static void assertSuggestion(Suggest searchSuggest, int entry, int ord, String key, String text) {
        assertThat(searchSuggest, notNullValue());
        String msg = "Suggest result: " + searchSuggest;
        assertThat(msg, searchSuggest.size(), greaterThanOrEqualTo(1));
        assertThat(msg, searchSuggest.getSuggestion(key).getName(), equalTo(key));
        assertThat(msg, searchSuggest.getSuggestion(key).getEntries(), hasSize(greaterThanOrEqualTo(entry)));
        assertThat(msg, searchSuggest.getSuggestion(key).getEntries().get(entry).getOptions(), hasSize(greaterThan(ord)));
        assertThat(msg, searchSuggest.getSuggestion(key).getEntries().get(entry).getOptions().get(ord).getText().string(), equalTo(text));
    }

    /**
     * Assert suggestion returns exactly the provided text.
     */
    public static void assertSuggestion(Suggest searchSuggest, int entry, String key, String... text) {
        assertSuggestion(searchSuggest, entry, key, text.length, text);
    }

    /**
     * Assert suggestion returns size suggestions and the first are the provided
     * text.
     */
    public static void assertSuggestion(Suggest searchSuggest, int entry, String key, int size, String... text) {
        assertSuggestionSize(searchSuggest, entry, size, key);
        for (int i = 0; i < text.length; i++) {
            assertSuggestion(searchSuggest, entry, i, key, text[i]);
        }
    }

    /**
     * Assert that an index template is missing
     */
    public static void assertIndexTemplateMissing(GetIndexTemplatesResponse templatesResponse, String name) {
        assertThat(templatesResponse.getIndexTemplates(), not(hasItem(transformedMatch(IndexTemplateMetadata::name, equalTo(name)))));
    }

    /**
     * Assert that an index template exists
     */
    public static void assertIndexTemplateExists(GetIndexTemplatesResponse templatesResponse, String name) {
        assertThat(templatesResponse.getIndexTemplates(), hasItem(transformedMatch(IndexTemplateMetadata::name, equalTo(name))));
    }

    /*
     * matchers
     */
    public static Matcher hasId(final String id) {
        return transformedMatch(SearchHit::getId, equalTo(id));
    }

    public static Matcher hasIndex(final String index) {
        return transformedMatch(SearchHit::getIndex, equalTo(index));
    }

    public static Matcher hasScore(final float score) {
        return transformedMatch(SearchHit::getScore, equalTo(score));
    }

    public static Matcher hasRank(final int rank) {
        return transformedMatch(SearchHit::getRank, equalTo(rank));
    }

    public static  T assertBooleanSubQuery(Query query, Class subqueryType, int i) {
        assertThat(query, instanceOf(BooleanQuery.class));
        BooleanQuery q = (BooleanQuery) query;
        assertThat(q.clauses(), hasSize(greaterThan(i)));
        assertThat(q.clauses().get(i).getQuery(), instanceOf(subqueryType));
        return subqueryType.cast(q.clauses().get(i).getQuery());
    }

    /**
     * Run the request from a given builder and check that it throws an exception of the right type, with a given {@link RestStatus}
     */
    public static  void assertRequestBuilderThrows(
        RequestBuilder builder,
        Class exceptionClass,
        RestStatus status
    ) {
        assertFutureThrows(builder.execute(), exceptionClass, status);
    }

    /**
     * Run the request from a given builder and check that it throws an exception of the right type
     *
     * @param extraInfo extra information to add to the failure message
     */
    public static  void assertRequestBuilderThrows(
        RequestBuilder builder,
        Class exceptionClass,
        String extraInfo
    ) {
        assertFutureThrows(builder.execute(), exceptionClass, extraInfo);
    }

    /**
     * Run future.actionGet() and check that it throws an exception of the right type
     */
    public static  void assertFutureThrows(ActionFuture future, Class exceptionClass) {
        assertFutureThrows(future, exceptionClass, null, null);
    }

    /**
     * Run future.actionGet() and check that it throws an exception of the right type, with a given {@link RestStatus}
     */
    public static  void assertFutureThrows(ActionFuture future, Class exceptionClass, RestStatus status) {
        assertFutureThrows(future, exceptionClass, status, null);
    }

    /**
     * Run future.actionGet() and check that it throws an exception of the right type
     *
     * @param extraInfo extra information to add to the failure message
     */
    public static  void assertFutureThrows(ActionFuture future, Class exceptionClass, String extraInfo) {
        assertFutureThrows(future, exceptionClass, null, extraInfo);
    }

    /**
     * Run future.actionGet() and check that it throws an exception of the right type, optionally checking the exception's rest status
     *
     * @param exceptionClass expected exception class
     * @param status         {@link org.elasticsearch.rest.RestStatus} to check for. Can be null to disable the check
     * @param extraInfo      extra information to add to the failure message. Can be null.
     */
    public static  void assertFutureThrows(
        ActionFuture future,
        Class exceptionClass,
        @Nullable RestStatus status,
        @Nullable String extraInfo
    ) {
        extraInfo = extraInfo == null || extraInfo.isEmpty() ? "" : extraInfo + ": ";
        extraInfo += "expected a " + exceptionClass + " exception to be thrown";

        if (status != null) {
            extraInfo += " with status [" + status + "]";
        }

        Throwable expected = expectThrowsAnyOf(List.of(exceptionClass, ElasticsearchException.class), future::actionGet);
        if (expected instanceof ElasticsearchException) {
            assertThat(extraInfo, ((ElasticsearchException) expected).unwrapCause(), instanceOf(exceptionClass));
        } else {
            assertThat(extraInfo, expected, instanceOf(exceptionClass));
        }

        if (status != null) {
            assertThat(extraInfo, ExceptionsHelper.status(expected), equalTo(status));
        }
    }

    public static void assertRequestBuilderThrows(RequestBuilder builder, RestStatus status) {
        assertFutureThrows(builder.execute(), status);
    }

    public static void assertRequestBuilderThrows(RequestBuilder builder, RestStatus status, String extraInfo) {
        assertFutureThrows(builder.execute(), status, extraInfo);
    }

    public static void assertFutureThrows(ActionFuture future, RestStatus status) {
        assertFutureThrows(future, status, null);
    }

    public static void assertFutureThrows(ActionFuture future, RestStatus status, String extraInfo) {
        extraInfo = extraInfo == null || extraInfo.isEmpty() ? "" : extraInfo + ": ";
        extraInfo += "expected a " + status + " status exception to be thrown";

        Exception e = expectThrows(Exception.class, future::actionGet);
        assertThat(extraInfo, ExceptionsHelper.status(e), equalTo(status));
    }

    /**
     * Check if a file exists
     */
    public static void assertFileExists(Path file) {
        assertThat("file/dir [" + file + "] should exist.", Files.exists(file), is(true));
    }

    /**
     * Check if a file does not exist
     */
    public static void assertFileNotExists(Path file) {
        assertThat("file/dir [" + file + "] should not exist.", Files.exists(file), is(false));
    }

    /**
     * Asserts that the provided {@link BytesReference}s created through
     * {@link ToXContent#toXContent(XContentBuilder, ToXContent.Params)} hold the same content.
     * The comparison is done by parsing both into a map and comparing those two, so that keys ordering doesn't matter.
     * Also binary values (byte[]) are properly compared through arrays comparisons.
     */
    public static void assertToXContentEquivalent(BytesReference expected, BytesReference actual, XContentType xContentType)
        throws IOException {
        // we tried comparing byte per byte, but that didn't fly for a couple of reasons:
        // 1) whenever anything goes through a map while parsing, ordering is not preserved, which is perfectly ok
        // 2) Jackson SMILE parser parses floats as double, which then get printed out as double (with double precision)
        // Note that byte[] holding binary values need special treatment as they need to be properly compared item per item.
        Map actualMap = null;
        Map expectedMap = null;
        try (
            XContentParser actualParser = XContentHelper.createParserNotCompressed(XContentParserConfiguration.EMPTY, actual, xContentType)
        ) {
            actualMap = actualParser.map();
            try (
                XContentParser expectedParser = XContentHelper.createParserNotCompressed(
                    XContentParserConfiguration.EMPTY,
                    expected,
                    xContentType
                )
            ) {
                expectedMap = expectedParser.map();
                try {
                    assertMapEquals(expectedMap, actualMap);
                } catch (AssertionError error) {
                    NotEqualMessageBuilder message = new NotEqualMessageBuilder();
                    message.compareMaps(actualMap, expectedMap);
                    throw new AssertionError("Error when comparing xContent.\n" + message.toString(), error);
                }
            }
        }
    }

    /**
     * Wait for a latch to countdown and provide a useful error message if it does not
     * Often latches are called as assertTrue(latch.await(1, TimeUnit.SECONDS));
     * In case of a failure this will just throw an assertion error without any further message
     *
     * @param latch   The latch to wait for
     * @param timeout The value of the timeout
     * @param unit    The unit of the timeout
     * @throws InterruptedException An exception if the waiting is interrupted
     */
    public static void awaitLatch(CountDownLatch latch, long timeout, TimeUnit unit) throws InterruptedException {
        TimeValue timeValue = new TimeValue(timeout, unit);
        String message = String.format(Locale.ROOT, "expected latch to be counted down after %s, but was not", timeValue);
        boolean isCountedDown = latch.await(timeout, unit);
        assertThat(message, isCountedDown, is(true));
    }

    /**
     * Compares two maps recursively, using arrays comparisons for byte[] through Arrays.equals(byte[], byte[])
     */
    private static void assertMapEquals(Map expected, Map actual) {
        assertEquals(expected.size(), actual.size());
        for (Map.Entry expectedEntry : expected.entrySet()) {
            String expectedKey = expectedEntry.getKey();
            Object expectedValue = expectedEntry.getValue();
            if (expectedValue == null) {
                assertTrue(actual.get(expectedKey) == null && actual.containsKey(expectedKey));
            } else {
                Object actualValue = actual.get(expectedKey);
                assertObjectEquals(expectedValue, actualValue);
            }
        }
    }

    /**
     * Compares two lists recursively, but using arrays comparisons for byte[] through Arrays.equals(byte[], byte[])
     */
    private static void assertListEquals(List expected, List actual) {
        assertEquals(expected.size(), actual.size());
        Iterator actualIterator = actual.iterator();
        for (Object expectedValue : expected) {
            Object actualValue = actualIterator.next();
            assertObjectEquals(expectedValue, actualValue);
        }
    }

    /**
     * Compares two objects, recursively walking eventual maps and lists encountered, and using arrays comparisons
     * for byte[] through Arrays.equals(byte[], byte[])
     */
    @SuppressWarnings("unchecked")
    private static void assertObjectEquals(Object expected, Object actual) {
        if (expected instanceof Map) {
            assertThat(actual, instanceOf(Map.class));
            assertMapEquals((Map) expected, (Map) actual);
        } else if (expected instanceof List) {
            assertListEquals((List) expected, (List) actual);
        } else if (expected instanceof byte[]) {
            // byte[] is really a special case for binary values when comparing SMILE and CBOR, arrays of other types
            // don't need to be handled. Ordinary arrays get parsed as lists.
            assertArrayEquals((byte[]) expected, (byte[]) actual);
        } else {
            assertEquals(expected, actual);
        }
    }
}