diff --git a/core/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java b/core/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java index 39b882f1a1d..12f3a5d3be6 100644 --- a/core/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java +++ b/core/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java @@ -21,13 +21,21 @@ package org.elasticsearch.common.logging; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.Build; +import org.elasticsearch.Version; import org.elasticsearch.common.SuppressLoggerChecks; import org.elasticsearch.common.util.concurrent.ThreadContext; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Iterator; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A logger that logs deprecation notices. @@ -36,14 +44,6 @@ public class DeprecationLogger { private final Logger logger; - /** - * The "Warning" Header comes from RFC-7234. As the RFC describes, it's generally used for caching purposes, but it can be - * used for any warning. - * - * https://tools.ietf.org/html/rfc7234#section-5.5 - */ - public static final String WARNING_HEADER = "Warning"; - /** * This is set once by the {@code Node} constructor, but it uses {@link CopyOnWriteArraySet} to ensure that tests can run in parallel. *

@@ -112,6 +112,57 @@ public class DeprecationLogger { deprecated(THREAD_CONTEXT, msg, params); } + /* + * RFC7234 specifies the warning format as warn-code warn-agent "warn-text" [ "warn-date"]. Here, warn-code is a + * three-digit number with various standard warn codes specified. The warn code 299 is apt for our purposes as it represents a + * miscellaneous persistent warning (can be presented to a human, or logged, and must not be removed by a cache). The warn-agent is an + * arbitrary token; here we use the Elasticsearch version and build hash. The warn text must be quoted. The warn-date is an optional + * quoted field that can be in a variety of specified date formats; here we use RFC 1123 format. + */ + private static final String WARNING_FORMAT = + String.format( + Locale.ROOT, + "299 Elasticsearch-%s%s-%s ", + Version.CURRENT.toString(), + Build.CURRENT.isSnapshot() ? "-SNAPSHOT" : "", + Build.CURRENT.shortHash()) + + "\"%s\" \"%s\""; + + private static final ZoneId GMT = ZoneId.of("GMT"); + + /** + * Regular expression to test if a string matches the RFC7234 specification for warning headers. This pattern assumes that the warn code + * is always 299. Further, this pattern assumes that the warn agent represents a version of Elasticsearch including the build hash. + */ + public static Pattern WARNING_HEADER_PATTERN = Pattern.compile( + "299 " + // warn code + "Elasticsearch-\\d+\\.\\d+\\.\\d+(?:-(?:alpha|beta|rc)\\d+)?(?:-SNAPSHOT)?-(?:[a-f0-9]{7}|Unknown) " + // warn agent + "\"((?:\t| |!|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x80-\\xff]|\\\\|\\\\\")*)\" " + // quoted warning value, captured + // quoted RFC 1123 date format + "\"" + // opening quote + "(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), " + // weekday + "\\d{2} " + // 2-digit day + "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) " + // month + "\\d{4} " + // 4-digit year + "\\d{2}:\\d{2}:\\d{2} " + // (two-digit hour):(two-digit minute):(two-digit second) + "GMT" + // GMT + "\""); // closing quote + + /** + * Extracts the warning value from the value of a warning header that is formatted according to RFC 7234. That is, given a string + * {@code 299 Elasticsearch-6.0.0 "warning value" "Sat, 25 Feb 2017 10:27:43 GMT"}, the return value of this method would be {@code + * warning value}. + * + * @param s the value of a warning header formatted according to RFC 7234. + * @return the extracted warning value + */ + public static String extractWarningValueFromWarningHeader(final String s) { + final Matcher matcher = WARNING_HEADER_PATTERN.matcher(s); + final boolean matches = matcher.matches(); + assert matches; + return matcher.group(1); + } + /** * Logs a deprecated message to the deprecation log, as well as to the local {@link ThreadContext}. * @@ -120,16 +171,19 @@ public class DeprecationLogger { * @param params The parameters used to fill in the message, if any exist. */ @SuppressLoggerChecks(reason = "safely delegates to logger") - void deprecated(Set threadContexts, String message, Object... params) { - Iterator iterator = threadContexts.iterator(); + void deprecated(final Set threadContexts, final String message, final Object... params) { + final Iterator iterator = threadContexts.iterator(); if (iterator.hasNext()) { final String formattedMessage = LoggerMessageFormat.format(message, params); - + final String warningHeaderValue = formatWarning(formattedMessage); + assert WARNING_HEADER_PATTERN.matcher(warningHeaderValue).matches(); + assert extractWarningValueFromWarningHeader(warningHeaderValue).equals(escape(formattedMessage)); while (iterator.hasNext()) { try { - iterator.next().addResponseHeader(WARNING_HEADER, formattedMessage); - } catch (IllegalStateException e) { + final ThreadContext next = iterator.next(); + next.addResponseHeader("Warning", warningHeaderValue, DeprecationLogger::extractWarningValueFromWarningHeader); + } catch (final IllegalStateException e) { // ignored; it should be removed shortly } } @@ -139,4 +193,25 @@ public class DeprecationLogger { } } + /** + * Format a warning string in the proper warning format by prepending a warn code, warn agent, wrapping the warning string in quotes, + * and appending the RFC 1123 date. + * + * @param s the warning string to format + * @return a warning value formatted according to RFC 7234 + */ + public static String formatWarning(final String s) { + return String.format(Locale.ROOT, WARNING_FORMAT, escape(s), DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(GMT))); + } + + /** + * Escape backslashes and quotes in the specified string. + * + * @param s the string to escape + * @return the escaped string + */ + public static String escape(String s) { + return s.replaceAll("(\\\\|\")", "\\\\$1"); + } + } diff --git a/core/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java b/core/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java index d439696b720..409a70eb649 100644 --- a/core/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java +++ b/core/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java @@ -34,7 +34,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -257,12 +259,25 @@ public final class ThreadContext implements Closeable, Writeable { } /** - * Add the unique response {@code value} for the specified {@code key}. - *

- * Any duplicate {@code value} is ignored. + * Add the {@code value} for the specified {@code key} Any duplicate {@code value} is ignored. + * + * @param key the header name + * @param value the header value */ - public void addResponseHeader(String key, String value) { - threadLocal.set(threadLocal.get().putResponse(key, value)); + public void addResponseHeader(final String key, final String value) { + addResponseHeader(key, value, v -> v); + } + + /** + * Add the {@code value} for the specified {@code key} with the specified {@code uniqueValue} used for de-duplication. Any duplicate + * {@code value} after applying {@code uniqueValue} is ignored. + * + * @param key the header name + * @param value the header value + * @param uniqueValue the function that produces de-duplication values + */ + public void addResponseHeader(final String key, final String value, final Function uniqueValue) { + threadLocal.set(threadLocal.get().putResponse(key, value, uniqueValue)); } /** @@ -396,14 +411,16 @@ public final class ThreadContext implements Closeable, Writeable { return new ThreadContextStruct(requestHeaders, newResponseHeaders, transientHeaders); } - private ThreadContextStruct putResponse(String key, String value) { + private ThreadContextStruct putResponse(final String key, final String value, final Function uniqueValue) { assert value != null; final Map> newResponseHeaders = new HashMap<>(this.responseHeaders); final List existingValues = newResponseHeaders.get(key); if (existingValues != null) { - if (existingValues.contains(value)) { + final Set existingUniqueValues = existingValues.stream().map(uniqueValue).collect(Collectors.toSet()); + assert existingValues.size() == existingUniqueValues.size(); + if (existingUniqueValues.contains(uniqueValue.apply(value))) { return this; } diff --git a/core/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java b/core/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java index d0e1b807baf..562b9b97eb3 100644 --- a/core/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java +++ b/core/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java @@ -18,9 +18,11 @@ */ package org.elasticsearch.common.logging; +import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.hamcrest.RegexMatcher; import java.io.IOException; import java.util.Collections; @@ -28,17 +30,22 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.IntStream; -import static org.hamcrest.Matchers.not; +import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN; +import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; /** * Tests {@link DeprecationLogger} */ public class DeprecationLoggerTests extends ESTestCase { + private static final RegexMatcher warningValueMatcher = matches(WARNING_HEADER_PATTERN.pattern()); + private final DeprecationLogger logger = new DeprecationLogger(Loggers.getLogger(getClass())); @Override @@ -48,43 +55,42 @@ public class DeprecationLoggerTests extends ESTestCase { } public void testAddsHeaderWithThreadContext() throws IOException { - String msg = "A simple message [{}]"; - String param = randomAsciiOfLengthBetween(1, 5); - String formatted = LoggerMessageFormat.format(msg, (Object)param); - try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) { - Set threadContexts = Collections.singleton(threadContext); + final Set threadContexts = Collections.singleton(threadContext); - logger.deprecated(threadContexts, msg, param); + final String param = randomAsciiOfLengthBetween(1, 5); + logger.deprecated(threadContexts, "A simple message [{}]", param); - Map> responseHeaders = threadContext.getResponseHeaders(); + final Map> responseHeaders = threadContext.getResponseHeaders(); - assertEquals(1, responseHeaders.size()); - assertEquals(formatted, responseHeaders.get(DeprecationLogger.WARNING_HEADER).get(0)); + assertThat(responseHeaders.size(), equalTo(1)); + final List responses = responseHeaders.get("Warning"); + assertThat(responses, hasSize(1)); + assertThat(responses.get(0), warningValueMatcher); + assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\"")); } } public void testAddsCombinedHeaderWithThreadContext() throws IOException { - String msg = "A simple message [{}]"; - String param = randomAsciiOfLengthBetween(1, 5); - String formatted = LoggerMessageFormat.format(msg, (Object)param); - String formatted2 = randomAsciiOfLengthBetween(1, 10); - try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) { - Set threadContexts = Collections.singleton(threadContext); + final Set threadContexts = Collections.singleton(threadContext); - logger.deprecated(threadContexts, msg, param); - logger.deprecated(threadContexts, formatted2); + final String param = randomAsciiOfLengthBetween(1, 5); + logger.deprecated(threadContexts, "A simple message [{}]", param); + final String second = randomAsciiOfLengthBetween(1, 10); + logger.deprecated(threadContexts, second); - Map> responseHeaders = threadContext.getResponseHeaders(); + final Map> responseHeaders = threadContext.getResponseHeaders(); assertEquals(1, responseHeaders.size()); - List responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER); + final List responses = responseHeaders.get("Warning"); assertEquals(2, responses.size()); - assertEquals(formatted, responses.get(0)); - assertEquals(formatted2, responses.get(1)); + assertThat(responses.get(0), warningValueMatcher); + assertThat(responses.get(0), containsString("\"A simple message [" + param + "]\"")); + assertThat(responses.get(1), warningValueMatcher); + assertThat(responses.get(1), containsString("\"" + second + "\"")); } } @@ -93,28 +99,30 @@ public class DeprecationLoggerTests extends ESTestCase { final String unexpected = "testCannotRemoveThreadContext"; try (ThreadContext threadContext = new ThreadContext(Settings.EMPTY)) { - // NOTE: by adding it to the logger, we allow any concurrent test to write to it (from their own threads) DeprecationLogger.setThreadContext(threadContext); - logger.deprecated(expected); - Map> responseHeaders = threadContext.getResponseHeaders(); - List responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER); + { + final Map> responseHeaders = threadContext.getResponseHeaders(); + final List responses = responseHeaders.get("Warning"); - // ensure it works (note: concurrent tests may be adding to it, but in different threads, so it should have no impact) - assertThat(responses, hasSize(atLeast(1))); - assertThat(responses, hasItem(equalTo(expected))); + assertThat(responses, hasSize(1)); + assertThat(responses.get(0), warningValueMatcher); + assertThat(responses.get(0), containsString(expected)); + } DeprecationLogger.removeThreadContext(threadContext); - logger.deprecated(unexpected); - responseHeaders = threadContext.getResponseHeaders(); - responses = responseHeaders.get(DeprecationLogger.WARNING_HEADER); + { + final Map> responseHeaders = threadContext.getResponseHeaders(); + final List responses = responseHeaders.get("Warning"); - assertThat(responses, hasSize(atLeast(1))); - assertThat(responses, hasItem(expected)); - assertThat(responses, not(hasItem(unexpected))); + assertThat(responses, hasSize(1)); + assertThat(responses.get(0), warningValueMatcher); + assertThat(responses.get(0), containsString(expected)); + assertThat(responses.get(0), not(containsString(unexpected))); + } } } @@ -158,4 +166,28 @@ public class DeprecationLoggerTests extends ESTestCase { } } + public void testWarningValueFromWarningHeader() throws InterruptedException { + final String s = randomAsciiOfLength(16); + final String first = DeprecationLogger.formatWarning(s); + assertThat(DeprecationLogger.extractWarningValueFromWarningHeader(first), equalTo(s)); + } + + public void testEscape() { + assertThat(DeprecationLogger.escape("\\"), equalTo("\\\\")); + assertThat(DeprecationLogger.escape("\""), equalTo("\\\"")); + assertThat(DeprecationLogger.escape("\\\""), equalTo("\\\\\\\"")); + assertThat(DeprecationLogger.escape("\"foo\\bar\""),equalTo("\\\"foo\\\\bar\\\"")); + // test that characters other than '\' and '"' are left unchanged + String chars = "\t !" + range(0x23, 0x5b + 1) + range(0x5d, 0x73 + 1) + range(0x80, 0xff + 1); + final String s = new CodepointSetGenerator(chars.toCharArray()).ofCodePointsLength(random(), 16, 16); + assertThat(DeprecationLogger.escape(s), equalTo(s)); + } + + private String range(int lowerInclusive, int upperInclusive) { + return IntStream + .range(lowerInclusive, upperInclusive + 1) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + } diff --git a/core/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java b/core/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java index 582f5d58d80..698e216ea16 100644 --- a/core/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java +++ b/core/src/test/java/org/elasticsearch/common/util/concurrent/ThreadContextTests.java @@ -19,6 +19,7 @@ package org.elasticsearch.common.util.concurrent; import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; @@ -178,6 +179,14 @@ public class ThreadContextTests extends ESTestCase { threadContext.addResponseHeader("foo", "bar"); } + final String value = DeprecationLogger.formatWarning("qux"); + threadContext.addResponseHeader("baz", value, DeprecationLogger::extractWarningValueFromWarningHeader); + // pretend that another thread created the same response at a different time + if (randomBoolean()) { + final String duplicateValue = DeprecationLogger.formatWarning("qux"); + threadContext.addResponseHeader("baz", duplicateValue, DeprecationLogger::extractWarningValueFromWarningHeader); + } + threadContext.addResponseHeader("Warning", "One is the loneliest number"); threadContext.addResponseHeader("Warning", "Two can be as bad as one"); if (expectThird) { @@ -186,11 +195,14 @@ public class ThreadContextTests extends ESTestCase { final Map> responseHeaders = threadContext.getResponseHeaders(); final List foo = responseHeaders.get("foo"); + final List baz = responseHeaders.get("baz"); final List warnings = responseHeaders.get("Warning"); final int expectedWarnings = expectThird ? 3 : 2; assertThat(foo, hasSize(1)); + assertThat(baz, hasSize(1)); assertEquals("bar", foo.get(0)); + assertEquals(value, baz.get(0)); assertThat(warnings, hasSize(expectedWarnings)); assertThat(warnings, hasItem(equalTo("One is the loneliest number"))); assertThat(warnings, hasItem(equalTo("Two can be as bad as one"))); diff --git a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/plugin/discovery/azure/classic/AzureDiscoveryPlugin.java b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/plugin/discovery/azure/classic/AzureDiscoveryPlugin.java index 1661c69c725..38e29e04583 100644 --- a/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/plugin/discovery/azure/classic/AzureDiscoveryPlugin.java +++ b/plugins/discovery-azure-classic/src/main/java/org/elasticsearch/plugin/discovery/azure/classic/AzureDiscoveryPlugin.java @@ -102,9 +102,9 @@ public class AzureDiscoveryPlugin extends Plugin implements DiscoveryPlugin { // setting existed. This check looks for the legacy setting, and sets hosts provider if set String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings); if (discoveryType.equals(AZURE)) { - deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() + - " setting to set hosts provider is deprecated. " + - "Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead"); + deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() + + "] to set hosts provider is deprecated; " + + "set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + AZURE + "\" instead"); if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) { return Settings.builder().put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), AZURE).build(); } diff --git a/plugins/discovery-ec2/src/main/java/org/elasticsearch/plugin/discovery/ec2/Ec2DiscoveryPlugin.java b/plugins/discovery-ec2/src/main/java/org/elasticsearch/plugin/discovery/ec2/Ec2DiscoveryPlugin.java index b3209ec571e..0529404ea9f 100644 --- a/plugins/discovery-ec2/src/main/java/org/elasticsearch/plugin/discovery/ec2/Ec2DiscoveryPlugin.java +++ b/plugins/discovery-ec2/src/main/java/org/elasticsearch/plugin/discovery/ec2/Ec2DiscoveryPlugin.java @@ -165,9 +165,9 @@ public class Ec2DiscoveryPlugin extends Plugin implements DiscoveryPlugin, Close // setting existed. This check looks for the legacy setting, and sets hosts provider if set String discoveryType = DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings); if (discoveryType.equals(EC2)) { - deprecationLogger.deprecated("Using " + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() + - " setting to set hosts provider is deprecated. " + - "Set \"" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "\" instead"); + deprecationLogger.deprecated("using [" + DiscoveryModule.DISCOVERY_TYPE_SETTING.getKey() + + "] setting to set hosts provider is deprecated; " + + "set [" + DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey() + ": " + EC2 + "] instead"); if (DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.exists(settings) == false) { builder.put(DiscoveryModule.DISCOVERY_HOSTS_PROVIDER_SETTING.getKey(), EC2).build(); } diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/cloud/aws/AwsS3ServiceImplTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/cloud/aws/AwsS3ServiceImplTests.java index 09a3222d63e..e13aa61f3a2 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/cloud/aws/AwsS3ServiceImplTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/cloud/aws/AwsS3ServiceImplTests.java @@ -23,7 +23,6 @@ import com.amazonaws.ClientConfiguration; import com.amazonaws.Protocol; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; - import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.repositories.s3.S3Repository; @@ -65,8 +64,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(AwsS3Service.SECRET_SETTING.getKey(), "aws_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "aws_key", "aws_secret"); - assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(AwsS3Service.KEY_SETTING, AwsS3Service.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchS3SettingsBackcompat() { @@ -75,8 +73,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret"); - assertWarnings("[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(AwsS3Service.CLOUD_S3.KEY_SETTING, AwsS3Service.CLOUD_S3.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchAwsAndS3SettingsBackcompat() { @@ -87,10 +84,11 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey(), "s3_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "s3_key", "s3_secret"); - assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations( + AwsS3Service.KEY_SETTING, + AwsS3Service.SECRET_SETTING, + AwsS3Service.CLOUD_S3.KEY_SETTING, + AwsS3Service.CLOUD_S3.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchRepositoriesSettingsBackcompat() { @@ -99,8 +97,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret"); - assertWarnings("[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repositories.KEY_SETTING, S3Repository.Repositories.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsBackcompat() { @@ -111,10 +108,11 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret"); - assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations( + AwsS3Service.KEY_SETTING, + AwsS3Service.SECRET_SETTING, + S3Repository.Repositories.KEY_SETTING, + S3Repository.Repositories.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsBackcompat() { @@ -127,12 +125,13 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(Settings.EMPTY, settings, "repositories_key", "repositories_secret"); - assertWarnings("[" + AwsS3Service.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SECRET_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.SECRET_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repositories.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repositories.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations( + AwsS3Service.KEY_SETTING, + AwsS3Service.SECRET_SETTING, + AwsS3Service.CLOUD_S3.KEY_SETTING, + AwsS3Service.CLOUD_S3.SECRET_SETTING, + S3Repository.Repositories.KEY_SETTING, + S3Repository.Repositories.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchRepositoriesSettingsAndRepositorySettingsBackcompat() { @@ -142,8 +141,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret"); - assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchAwsAndRepositoriesSettingsAndRepositorySettingsBackcompat() { @@ -155,8 +153,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret"); - assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING); } public void testAWSCredentialsWithElasticsearchAwsAndS3AndRepositoriesSettingsAndRepositorySettingsBackcompat() { @@ -170,8 +167,7 @@ public class AwsS3ServiceImplTests extends ESTestCase { .put(S3Repository.Repositories.SECRET_SETTING.getKey(), "repositories_secret") .build(); launchAWSCredentialsWithElasticsearchSettingsTest(repositorySettings, settings, "repository_key", "repository_secret"); - assertWarnings("[" + S3Repository.Repository.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + S3Repository.Repository.SECRET_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repository.KEY_SETTING, S3Repository.Repository.SECRET_SETTING); } protected void launchAWSCredentialsWithElasticsearchSettingsTest(Settings singleRepositorySettings, Settings settings, @@ -215,13 +211,14 @@ public class AwsS3ServiceImplTests extends ESTestCase { .build(); launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTP, "aws_proxy_host", 8080, "aws_proxy_username", "aws_proxy_password", "AWS3SignerType", 3, false, 10000); - assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated"); + assertSettingDeprecations( + AwsS3Service.PROXY_USERNAME_SETTING, + AwsS3Service.PROXY_PASSWORD_SETTING, + AwsS3Service.PROTOCOL_SETTING, + AwsS3Service.PROXY_HOST_SETTING, + AwsS3Service.PROXY_PORT_SETTING, + AwsS3Service.SIGNER_SETTING, + AwsS3Service.READ_TIMEOUT); } public void testAWSConfigurationWithAwsAndS3SettingsBackcompat() { @@ -243,20 +240,21 @@ public class AwsS3ServiceImplTests extends ESTestCase { .build(); launchAWSConfigurationTest(settings, Settings.EMPTY, Protocol.HTTPS, "s3_proxy_host", 8081, "s3_proxy_username", "s3_proxy_password", "NoOpSignerType", 3, false, 10000); - assertWarnings("[" + AwsS3Service.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROTOCOL_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_HOST_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.PROXY_PORT_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.SIGNER_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.READ_TIMEOUT.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.PROTOCOL_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.SIGNER_SETTING.getKey() + "] setting was deprecated", - "[" + AwsS3Service.CLOUD_S3.READ_TIMEOUT.getKey() + "] setting was deprecated"); + assertSettingDeprecations( + AwsS3Service.PROXY_USERNAME_SETTING, + AwsS3Service.PROXY_PASSWORD_SETTING, + AwsS3Service.PROTOCOL_SETTING, + AwsS3Service.PROXY_HOST_SETTING, + AwsS3Service.PROXY_PORT_SETTING, + AwsS3Service.SIGNER_SETTING, + AwsS3Service.READ_TIMEOUT, + AwsS3Service.CLOUD_S3.PROXY_USERNAME_SETTING, + AwsS3Service.CLOUD_S3.PROXY_PASSWORD_SETTING, + AwsS3Service.CLOUD_S3.PROTOCOL_SETTING, + AwsS3Service.CLOUD_S3.PROXY_HOST_SETTING, + AwsS3Service.CLOUD_S3.PROXY_PORT_SETTING, + AwsS3Service.CLOUD_S3.SIGNER_SETTING, + AwsS3Service.CLOUD_S3.READ_TIMEOUT); } public void testGlobalMaxRetries() { @@ -338,13 +336,13 @@ public class AwsS3ServiceImplTests extends ESTestCase { public void testEndpointSettingBackcompat() { assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", "repository.endpoint", null), Settings.EMPTY, "repository.endpoint"); - assertWarnings("[" + S3Repository.Repository.ENDPOINT_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repository.ENDPOINT_SETTING); Settings settings = Settings.builder() .put(S3Repository.Repositories.ENDPOINT_SETTING.getKey(), "repositories.endpoint") .build(); assertEndpoint(generateRepositorySettings("repository_key", "repository_secret", null, null), settings, "repositories.endpoint"); - assertWarnings("[" + S3Repository.Repositories.ENDPOINT_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(S3Repository.Repositories.ENDPOINT_SETTING); } private void assertEndpoint(Settings repositorySettings, Settings settings, diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java index 79fc453e0ed..f20c2a7ad30 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java @@ -19,8 +19,6 @@ package org.elasticsearch.repositories.s3; -import java.io.IOException; - import com.amazonaws.services.s3.AbstractAmazonS3; import com.amazonaws.services.s3.AmazonS3; import org.elasticsearch.cloud.aws.AwsS3Service; @@ -35,6 +33,8 @@ import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Matchers; +import java.io.IOException; + import static org.elasticsearch.repositories.s3.S3Repository.Repositories; import static org.elasticsearch.repositories.s3.S3Repository.Repository; import static org.elasticsearch.repositories.s3.S3Repository.getValue; @@ -78,8 +78,7 @@ public class S3RepositoryTests extends ESTestCase { getValue(Settings.EMPTY, globalSettings, Repository.KEY_SETTING, Repositories.KEY_SETTING)); assertEquals(new SecureString("".toCharArray()), getValue(Settings.EMPTY, Settings.EMPTY, Repository.KEY_SETTING, Repositories.KEY_SETTING)); - assertWarnings("[" + Repository.KEY_SETTING.getKey() + "] setting was deprecated", - "[" + Repositories.KEY_SETTING.getKey() + "] setting was deprecated"); + assertSettingDeprecations(Repository.KEY_SETTING, Repositories.KEY_SETTING); } public void testInvalidChunkBufferSizeSettings() throws IOException { diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 633a04f5f60..1a349ff0ef2 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -59,6 +59,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.MockBigArrays; @@ -134,10 +135,8 @@ import java.util.stream.Collectors; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.elasticsearch.common.util.CollectionUtils.arrayAsArrayList; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; /** * Base testcase for randomized unit testing with Elasticsearch @@ -297,21 +296,33 @@ public abstract class ESTestCase extends LuceneTestCase { //Check that there are no unaccounted warning headers. These should be checked with {@link #assertWarnings(String...)} in the //appropriate test try { - final List warnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER); + final List warnings = threadContext.getResponseHeaders().get("Warning"); assertNull("unexpected warning headers", warnings); } finally { resetDeprecationLogger(); } } + protected final void assertSettingDeprecations(Setting... settings) { + assertWarnings( + Arrays + .stream(settings) + .map(Setting::getKey) + .map(k -> "[" + k + "] setting was deprecated in Elasticsearch and will be removed in a future release! " + + "See the breaking changes documentation for the next major version.") + .toArray(String[]::new)); + } + protected final void assertWarnings(String... expectedWarnings) { if (enableWarningsCheck() == false) { throw new IllegalStateException("unable to check warning headers if the test is not set to do so"); } try { - final List actualWarnings = threadContext.getResponseHeaders().get(DeprecationLogger.WARNING_HEADER); + final List actualWarnings = threadContext.getResponseHeaders().get("Warning"); + final Set actualWarningValues = + actualWarnings.stream().map(DeprecationLogger::extractWarningValueFromWarningHeader).collect(Collectors.toSet()); for (String msg : expectedWarnings) { - assertThat(actualWarnings, hasItem(containsString(msg))); + assertTrue(actualWarningValues.contains(DeprecationLogger.escape(msg))); } assertEquals("Expected " + expectedWarnings.length + " warnings but found " + actualWarnings.size() + "\nExpected: " + Arrays.asList(expectedWarnings) + "\nActual: " + actualWarnings, diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java index 744b52303b5..5eb92220d90 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.test.rest.yaml.section; import org.apache.logging.log4j.Logger; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentLocation; @@ -39,10 +41,13 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.stream.Collectors; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static org.elasticsearch.common.collect.Tuple.tuple; +import static org.elasticsearch.common.logging.DeprecationLogger.WARNING_HEADER_PATTERN; import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; @@ -247,33 +252,48 @@ public class DoSection implements ExecutableSection { /** * Check that the response contains only the warning headers that we expect. */ - void checkWarningHeaders(List warningHeaders) { - StringBuilder failureMessage = null; + void checkWarningHeaders(final List warningHeaders) { + final List unexpected = new ArrayList<>(); + final List unmatched = new ArrayList<>(); + final List missing = new ArrayList<>(); // LinkedHashSet so that missing expected warnings come back in a predictable order which is nice for testing - Set expected = new LinkedHashSet<>(expectedWarningHeaders); - for (String header : warningHeaders) { - if (expected.remove(header)) { - // Was expected, all good. - continue; - } - if (failureMessage == null) { - failureMessage = new StringBuilder("got unexpected warning headers ["); - } - failureMessage.append('\n').append(header); - } - if (false == expected.isEmpty()) { - if (failureMessage == null) { - failureMessage = new StringBuilder(); + final Set expected = + new LinkedHashSet<>(expectedWarningHeaders.stream().map(DeprecationLogger::escape).collect(Collectors.toList())); + for (final String header : warningHeaders) { + final Matcher matcher = WARNING_HEADER_PATTERN.matcher(header); + final boolean matches = matcher.matches(); + if (matches) { + final String message = matcher.group(1); + if (expected.remove(message) == false) { + unexpected.add(header); + } } else { - failureMessage.append("\n] "); - } - failureMessage.append("didn't get expected warning headers ["); - for (String header : expected) { - failureMessage.append('\n').append(header); + unmatched.add(header); } } - if (failureMessage != null) { - fail(failureMessage + "\n]"); + if (expected.isEmpty() == false) { + for (final String header : expected) { + missing.add(header); + } + } + + if (unexpected.isEmpty() == false || unmatched.isEmpty() == false || missing.isEmpty() == false) { + final StringBuilder failureMessage = new StringBuilder(); + appendBadHeaders(failureMessage, unexpected, "got unexpected warning header" + (unexpected.size() > 1 ? "s" : "")); + appendBadHeaders(failureMessage, unmatched, "got unmatched warning header" + (unmatched.size() > 1 ? "s" : "")); + appendBadHeaders(failureMessage, missing, "did not get expected warning header" + (missing.size() > 1 ? "s" : "")); + fail(failureMessage.toString()); + } + + } + + private void appendBadHeaders(final StringBuilder sb, final List headers, final String message) { + if (headers.isEmpty() == false) { + sb.append(message).append(" [\n"); + for (final String header : headers) { + sb.append("\t").append(header).append("\n"); + } + sb.append("]\n"); } } diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java index 463e71341bb..2ff0f56d2b3 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java @@ -19,6 +19,9 @@ package org.elasticsearch.test.rest.yaml.section; +import org.elasticsearch.Version; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.xcontent.XContent; import org.elasticsearch.common.xcontent.XContentLocation; import org.elasticsearch.common.xcontent.XContentParser; @@ -26,6 +29,7 @@ import org.elasticsearch.common.xcontent.yaml.YamlXContent; import org.hamcrest.MatcherAssert; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Map; @@ -37,39 +41,80 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class DoSectionTests extends AbstractClientYamlTestFragmentParserTestCase { + public void testWarningHeaders() throws IOException { - DoSection section = new DoSection(new XContentLocation(1, 1)); + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); - // No warning headers doesn't throw an exception - section.checkWarningHeaders(emptyList()); + // No warning headers doesn't throw an exception + section.checkWarningHeaders(emptyList()); + } + final String testHeader = DeprecationLogger.formatWarning("test"); + final String anotherHeader = DeprecationLogger.formatWarning("another"); + final String someMoreHeader = DeprecationLogger.formatWarning("some more"); + final String catHeader = DeprecationLogger.formatWarning("cat"); // Any warning headers fail - AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList("test"))); - assertEquals("got unexpected warning headers [\ntest\n]", e.getMessage()); - e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "another", "some more"))); - assertEquals("got unexpected warning headers [\ntest\nanother\nsome more\n]", e.getMessage()); + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + + final AssertionError one = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(singletonList(testHeader))); + assertEquals("got unexpected warning header [\n\t" + testHeader + "\n]\n", one.getMessage()); + + final AssertionError multiple = + expectThrows( + AssertionError.class, + () -> section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader))); + assertEquals( + "got unexpected warning headers [\n\t" + + testHeader + "\n\t" + + anotherHeader + "\n\t" + + someMoreHeader + "\n]\n", + multiple.getMessage()); + } // But not when we expect them - section.setExpectedWarningHeaders(singletonList("test")); - section.checkWarningHeaders(singletonList("test")); - section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); - section.checkWarningHeaders(Arrays.asList("test", "another", "some more")); + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + section.setExpectedWarningHeaders(singletonList("test")); + section.checkWarningHeaders(singletonList(testHeader)); + } + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); + section.checkWarningHeaders(Arrays.asList(testHeader, anotherHeader, someMoreHeader)); + } // But if you don't get some that you did expect, that is an error - section.setExpectedWarningHeaders(singletonList("test")); - e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList())); - assertEquals("didn't get expected warning headers [\ntest\n]", e.getMessage()); - section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); - e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList())); - assertEquals("didn't get expected warning headers [\ntest\nanother\nsome more\n]", e.getMessage()); - e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "some more"))); - assertEquals("didn't get expected warning headers [\nanother\n]", e.getMessage()); + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + section.setExpectedWarningHeaders(singletonList("test")); + final AssertionError e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList())); + assertEquals("did not get expected warning header [\n\ttest\n]\n", e.getMessage()); + } + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); + + final AssertionError multiple = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(emptyList())); + assertEquals("did not get expected warning headers [\n\ttest\n\tanother\n\tsome more\n]\n", multiple.getMessage()); + + final AssertionError one = + expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, someMoreHeader))); + assertEquals("did not get expected warning header [\n\tanother\n]\n", one.getMessage()); + } // It is also an error if you get some warning you want and some you don't want - section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); - e = expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList("test", "cat"))); - assertEquals("got unexpected warning headers [\ncat\n] didn't get expected warning headers [\nanother\nsome more\n]", - e.getMessage()); + { + final DoSection section = new DoSection(new XContentLocation(1, 1)); + section.setExpectedWarningHeaders(Arrays.asList("test", "another", "some more")); + final AssertionError e = + expectThrows(AssertionError.class, () -> section.checkWarningHeaders(Arrays.asList(testHeader, catHeader))); + assertEquals("got unexpected warning header [\n\t" + + catHeader + "\n]\n" + + "did not get expected warning headers [\n\tanother\n\tsome more\n]\n", + e.getMessage()); + } } public void testParseDoSectionNoBody() throws Exception {