Skip to content

Commit

Permalink
[7.x] Add support for regex in REST test warnings and allowed_warnings (
Browse files Browse the repository at this point in the history
elastic#69501) (elastic#69754)

This commit adds support for two new REST test features.
warnings_regex and allowed_warnings_regex.

This is a near mirror of the warnings and allowed_warnings
warnings feature where the test can be instructed to allow
or require HTTP warnings. The difference with these new features
is that is allows the match to be based on a regular expression.
  • Loading branch information
jakelandis authored Mar 2, 2021
1 parent fa8bcd1 commit 53f8cd5
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,22 @@ somevalue with whatever is the response in the same position."

=== `warnings`

The runner can assert the warnings headers returned by Elasticsearch through the `warning:` assertations
under `do:` operations.
The runner can assert specific warnings headers are returned by Elasticsearch through the `warning:` assertations
under `do:` operations. The test will fail if the warning is not found.

=== `warnings_regex`

The same as `warnings`, but matches warning headers with the given regular expression.


=== `allowed_warnings`

The runner will allow specific warnings headers to be returned by Elasticsearch through the `allowed_warning:` assertations
under `do:` operations. The test will not fail if the warning is not found.

=== `allowed_warnings_regex`

The same as `allowed_warnings`, but matches warning headers with the given regular expression.

=== `yaml`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,7 @@ protected static void waitForActiveLicense(final RestClient restClient) throws E
});
}

//TODO: replace usages of this with warning_regex or allowed_warnings_regex
static final Pattern CREATE_INDEX_MULTIPLE_MATCHING_TEMPLATES = Pattern.compile("^index \\[(.+)\\] matches multiple legacy " +
"templates \\[(.+)\\], composable templates will only match a single template$");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ public final class Features {
"stash_in_path",
"stash_path_replace",
"warnings",
"warnings_regex",
"yaml",
"contains",
"transform_and_set",
"arbitrary_key",
"allowed_warnings"));

"allowed_warnings",
"allowed_warnings_regex"));
private static final String SPI_ON_CLASSPATH_SINCE_JDK_9 = "spi_on_classpath_jdk9";

private Features() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ private static Stream<String> validateExecutableSections(List<ExecutableSection>
"without a corresponding [\"skip\": \"features\": \"warnings\"] so runners that do not support the [warnings] " +
"section can skip the test at line [" + section.getLocation().lineNumber + "]");

errors = Stream.concat(errors, sections.stream().filter(section -> section instanceof DoSection)
.map(section -> (DoSection) section)
.filter(section -> false == section.getExpectedWarningHeadersRegex()
.isEmpty())
.filter(section -> false == hasSkipFeature("warnings_regex", testSection, setupSection, teardownSection))
.map(section -> "attempted to add a [do] with a [warnings_regex] section " +
"without a corresponding [\"skip\": \"features\": \"warnings_regex\"] so runners that do not support the [warnings_regex] "+
"section can skip the test at line [" + section.getLocation().lineNumber + "]"));

errors = Stream.concat(errors, sections.stream().filter(section -> section instanceof DoSection)
.map(section -> (DoSection) section)
.filter(section -> false == section.getAllowedWarningHeaders().isEmpty())
Expand All @@ -160,6 +169,14 @@ private static Stream<String> validateExecutableSections(List<ExecutableSection>
"without a corresponding [\"skip\": \"features\": \"allowed_warnings\"] so runners that do not " +
"support the [allowed_warnings] section can skip the test at line [" + section.getLocation().lineNumber + "]"));

errors = Stream.concat(errors, sections.stream().filter(section -> section instanceof DoSection)
.map(section -> (DoSection) section)
.filter(section -> false == section.getAllowedWarningHeadersRegex().isEmpty())
.filter(section -> false == hasSkipFeature("allowed_warnings_regex", testSection, setupSection, teardownSection))
.map(section -> "attempted to add a [do] with a [allowed_warnings_regex] section " +
"without a corresponding [\"skip\": \"features\": \"allowed_warnings_regex\"] so runners that do not " +
"support the [allowed_warnings_regex] section can skip the test at line [" + section.getLocation().lineNumber + "]"));

errors = Stream.concat(errors, sections.stream().filter(section -> section instanceof DoSection)
.map(section -> (DoSection) section)
.filter(section -> NodeSelector.ANY != section.getApiCallSection().getNodeSelector())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
Expand All @@ -39,11 +40,11 @@
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toSet;
import static org.elasticsearch.common.collect.Tuple.tuple;
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
import static org.hamcrest.Matchers.allOf;
Expand All @@ -62,13 +63,16 @@
* headers:
* Authorization: Basic user:pass
* Content-Type: application/json
* warnings:
* warnings|warnings_regex:
* - Stuff is deprecated, yo
* - Don't use deprecated stuff
* - Please, stop. It hurts.
* allowed_warnings:
* - The non _regex version exact matches against the warning text and no need to worry about escaped quotes or backslashes
* - The _regex version matches against the raw value of the warning text which may include backlashes and quotes escaped
* allowed_warnings|allowed_warnings_regex:
* - Maybe this warning shows up
* - But it isn't actually required for the test to pass.
* - The non _regex version should be preferred for simplicity and performance.
* update:
* index: test_1
* type: test
Expand All @@ -86,7 +90,9 @@ public static DoSection parse(XContentParser parser) throws IOException {
NodeSelector nodeSelector = NodeSelector.ANY;
Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
List<String> expectedWarnings = new ArrayList<>();
List<Pattern> expectedWarningsRegex = new ArrayList<>();
List<String> allowedWarnings = new ArrayList<>();
List<Pattern> allowedWarningsRegex = new ArrayList<>();

if (parser.nextToken() != XContentParser.Token.START_OBJECT) {
throw new IllegalArgumentException("expected [" + XContentParser.Token.START_OBJECT + "], " +
Expand All @@ -110,13 +116,29 @@ public static DoSection parse(XContentParser parser) throws IOException {
if (token != XContentParser.Token.END_ARRAY) {
throw new ParsingException(parser.getTokenLocation(), "[warnings] must be a string array but saw [" + token + "]");
}
} else if ("warnings_regex".equals(currentFieldName)) {
while ((token = parser.nextToken()) == XContentParser.Token.VALUE_STRING) {
expectedWarningsRegex.add(Pattern.compile(parser.text()));
}
if (token != XContentParser.Token.END_ARRAY) {
throw new ParsingException(parser.getTokenLocation(),
"[warnings_regex] must be a string array but saw [" + token + "]");
}
} else if ("allowed_warnings".equals(currentFieldName)) {
while ((token = parser.nextToken()) == XContentParser.Token.VALUE_STRING) {
allowedWarnings.add(parser.text());
}
if (token != XContentParser.Token.END_ARRAY) {
throw new ParsingException(parser.getTokenLocation(),
"[allowed_warnings] must be a string array but saw [" + token + "]");
"[allowed_warnings] must be a string array but saw [" + token + "]");
}
} else if ("allowed_warnings_regex".equals(currentFieldName)) {
while ((token = parser.nextToken()) == XContentParser.Token.VALUE_STRING) {
allowedWarningsRegex.add(Pattern.compile(parser.text()));
}
if (token != XContentParser.Token.END_ARRAY) {
throw new ParsingException(parser.getTokenLocation(),
"[allowed_warnings_regex] must be a string array but saw [" + token + "]");
}
} else {
throw new ParsingException(parser.getTokenLocation(), "unknown array [" + currentFieldName + "]");
Expand Down Expand Up @@ -178,11 +200,18 @@ public static DoSection parse(XContentParser parser) throws IOException {
throw new IllegalArgumentException("the warning [" + w + "] was both allowed and expected");
}
}
for (Pattern p : expectedWarningsRegex) {
if (allowedWarningsRegex.contains(p)) {
throw new IllegalArgumentException("the warning pattern [" + p + "] was both allowed and expected");
}
}
apiCallSection.addHeaders(headers);
apiCallSection.setNodeSelector(nodeSelector);
doSection.setApiCallSection(apiCallSection);
doSection.setExpectedWarningHeaders(unmodifiableList(expectedWarnings));
doSection.setExpectedWarningHeadersRegex(unmodifiableList(expectedWarningsRegex));
doSection.setAllowedWarningHeaders(unmodifiableList(allowedWarnings));
doSection.setAllowedWarningHeadersRegex(unmodifiableList(allowedWarningsRegex));
} finally {
parser.nextToken();
}
Expand All @@ -195,7 +224,9 @@ public static DoSection parse(XContentParser parser) throws IOException {
private String catchParam;
private ApiCallSection apiCallSection;
private List<String> expectedWarningHeaders = emptyList();
private List<Pattern> expectedWarningHeadersRegex = emptyList();
private List<String> allowedWarningHeaders = emptyList();
private List<Pattern> allowedWarningHeadersRegex = emptyList();

public DoSection(XContentLocation location) {
this.location = location;
Expand All @@ -218,13 +249,23 @@ void setApiCallSection(ApiCallSection apiCallSection) {
}

/**
* Warning headers that we expect from this response. If the headers don't match exactly this request is considered to have failed.
* Warning headers patterns that we expect from this response.
* If the headers don't match exactly this request is considered to have failed.
* Defaults to emptyList.
*/
List<String> getExpectedWarningHeaders() {
return expectedWarningHeaders;
}

/**
* Warning headers patterns that we expect from this response.
* If the headers don't match this request is considered to have failed.
* Defaults to emptyList.
*/
List<Pattern> getExpectedWarningHeadersRegex() {
return expectedWarningHeadersRegex;
}

/**
* Set the warning headers that we expect from this response. If the headers don't match exactly this request is considered to have
* failed. Defaults to emptyList.
Expand All @@ -233,6 +274,14 @@ void setExpectedWarningHeaders(List<String> expectedWarningHeaders) {
this.expectedWarningHeaders = expectedWarningHeaders;
}

/**
* Set the warning headers patterns that we expect from this response. If the headers don't match this request is considered to have
* failed. Defaults to emptyList.
*/
void setExpectedWarningHeadersRegex(List<Pattern> expectedWarningHeadersRegex) {
this.expectedWarningHeadersRegex = expectedWarningHeadersRegex;
}

/**
* Warning headers that we allow from this response. These warning
* headers don't cause the test to fail. Defaults to emptyList.
Expand All @@ -241,6 +290,14 @@ List<String> getAllowedWarningHeaders() {
return allowedWarningHeaders;
}

/**
* Warning headers that we allow from this response. These warning
* headers don't cause the test to fail. Defaults to emptyList.
*/
List<Pattern> getAllowedWarningHeadersRegex() {
return allowedWarningHeadersRegex;
}

/**
* Set the warning headers that we expect from this response. These
* warning headers don't cause the test to fail. Defaults to emptyList.
Expand All @@ -249,6 +306,14 @@ void setAllowedWarningHeaders(List<String> allowedWarningHeaders) {
this.allowedWarningHeaders = allowedWarningHeaders;
}

/**
* Set the warning headers pattern that we expect from this response. These
* warning headers don't cause the test to fail. Defaults to emptyList.
*/
void setAllowedWarningHeadersRegex(List<Pattern> allowedWarningHeadersRegex) {
this.allowedWarningHeadersRegex = allowedWarningHeadersRegex;
}

@Override
public XContentLocation getLocation() {
return location;
Expand Down Expand Up @@ -308,13 +373,16 @@ void checkWarningHeaders(final List<String> warningHeaders, final Version master
final List<String> unexpected = new ArrayList<>();
final List<String> unmatched = new ArrayList<>();
final List<String> missing = new ArrayList<>();
Set<String> allowed = allowedWarningHeaders.stream()
.map(HeaderWarning::escapeAndEncode)
.collect(toSet());
final List<String> missingRegex = new ArrayList<>();
// LinkedHashSet so that missing expected warnings come back in a predictable order which is nice for testing
final Set<String> allowed = allowedWarningHeaders.stream()
.map(HeaderWarning::escapeAndEncode)
.collect(toCollection(LinkedHashSet::new));
final Set<Pattern> allowedRegex = new LinkedHashSet<>(allowedWarningHeadersRegex);
final Set<String> expected = expectedWarningHeaders.stream()
.map(HeaderWarning::escapeAndEncode)
.collect(toCollection(LinkedHashSet::new));
final Set<Pattern> expectedRegex = new LinkedHashSet<>(expectedWarningHeadersRegex);
for (final String header : warningHeaders) {
final Matcher matcher = HeaderWarning.WARNING_HEADER_PATTERN.matcher(header);
final boolean matches = matcher.matches();
Expand All @@ -337,9 +405,28 @@ void checkWarningHeaders(final List<String> warningHeaders, final Version master
if (allowed.contains(message)) {
continue;
}

if (expected.remove(message)) {
continue;
}
boolean matchedRegex = false;

for(Pattern pattern : new HashSet<>(expectedRegex)){
if(pattern.matcher(message).matches()){
matchedRegex = true;
expectedRegex.remove(pattern);
break;
}
}
for(Pattern pattern : allowedRegex){
if(pattern.matcher(message).matches()){
matchedRegex = true;
break;
}
}
if (matchedRegex){
continue;
}
unexpected.add(header);
} else {
unmatched.add(header);
Expand All @@ -350,15 +437,24 @@ void checkWarningHeaders(final List<String> warningHeaders, final Version master
missing.add(header);
}
}
if (expectedRegex.isEmpty() == false) {
for (final Pattern headerPattern : expectedRegex) {
missingRegex.add(headerPattern.pattern());
}
}

if (unexpected.isEmpty() == false || unmatched.isEmpty() == false || missing.isEmpty() == false) {
if (unexpected.isEmpty() == false
|| unmatched.isEmpty() == false
|| missing.isEmpty() == false
|| missingRegex.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" : ""));
appendBadHeaders(failureMessage, missingRegex, "the following regular expression" + (missingRegex.size() > 1 ? "s" : "")
+ " did not match any warning header");
fail(failureMessage.toString());
}

}

private void appendBadHeaders(final StringBuilder sb, final List<String> headers, final String message) {
Expand Down
Loading

0 comments on commit 53f8cd5

Please sign in to comment.