diff --git a/test-framework/src/main/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcher.java b/test-framework/src/main/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcher.java new file mode 100644 index 00000000000..e5bb75955cf --- /dev/null +++ b/test-framework/src/main/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcher.java @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.test.rest; + +import java.util.regex.Pattern; + +/** + * Matches blacklist patterns. + * + * Currently the following syntax is supported: + * + * + * + * Each blacklist pattern is a suffix match on the path. Empty patterns are not allowed. + */ +final class BlacklistedPathPatternMatcher { + private final Pattern pattern; + + /** + * Constructs a new BlacklistedPathPatternMatcher instance from the provided suffix pattern. + * + * @param p The suffix pattern. Must be a non-empty string. + */ + BlacklistedPathPatternMatcher(String p) { + // guard against accidentally matching everything as an empty string lead to the pattern ".*" which matches everything + if (p == null || p.trim().isEmpty()) { + throw new IllegalArgumentException("Empty blacklist patterns are not supported"); + } + // very simple transformation from wildcard to a proper regex + String finalPattern = p + .replaceAll("\\*", "[^/]*") // support wildcard matches (within a single path segment) + .replaceAll("\\\\,", ","); // restore previously escaped ',' in paths. + + // suffix match + pattern = Pattern.compile(".*" + finalPattern); + } + + /** + * Checks whether the provided path matches the suffix pattern, i.e. "/foo/bar" will match the pattern "bar". + * + * @param path The path to match. Must not be null. + * @return true iff this path is a suffix match. + */ + public boolean isSuffixMatch(String path) { + return pattern.matcher(path).matches(); + } +} diff --git a/test-framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java b/test-framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java index 805808968c4..266f8e8038c 100644 --- a/test-framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java +++ b/test-framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java @@ -29,7 +29,6 @@ import org.apache.lucene.util.LuceneTestCase.SuppressFsync; import org.apache.lucene.util.TimeUnits; import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; -import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.node.Node; @@ -65,7 +64,6 @@ import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.PathMatcher; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; @@ -123,9 +121,18 @@ public abstract class ESRestTestCase extends ESIntegTestCase { private static final String DEFAULT_TESTS_PATH = "/rest-api-spec/test"; private static final String DEFAULT_SPEC_PATH = "/rest-api-spec/api"; - private static final String PATHS_SEPARATOR = ","; + /** + * This separator pattern matches ',' except it is preceded by a '\'. This allows us to support ',' within paths when it is escaped with + * a slash. + * + * For example, the path string "/a/b/c\,d/e/f,/foo/bar,/baz" is separated to "/a/b/c\,d/e/f", "/foo/bar" and "/baz". + * + * For reference, this regular expression feature is known as zero-width negative look-behind. + * + */ + private static final String PATHS_SEPARATOR = "(? blacklistPathMatchers = new ArrayList<>(); private static RestTestExecutionContext restTestExecutionContext; private final RestTestCandidate testCandidate; @@ -133,14 +140,8 @@ public abstract class ESRestTestCase extends ESIntegTestCase { public ESRestTestCase(RestTestCandidate testCandidate) { this.testCandidate = testCandidate; String[] blacklist = resolvePathsProperty(REST_TESTS_BLACKLIST, null); - if (blacklist != null) { - blacklistPathMatchers = new PathMatcher[blacklist.length]; - int i = 0; - for (String glob : blacklist) { - blacklistPathMatchers[i++] = PathUtils.getDefaultFileSystem().getPathMatcher("glob:" + glob); - } - } else { - blacklistPathMatchers = new PathMatcher[0]; + for (String entry : blacklist) { + this.blacklistPathMatchers.add(new BlacklistedPathPatternMatcher(entry)); } } @@ -226,7 +227,7 @@ public abstract class ESRestTestCase extends ESIntegTestCase { private static String[] resolvePathsProperty(String propertyName, String defaultValue) { String property = System.getProperty(propertyName); if (!Strings.hasLength(property)) { - return defaultValue == null ? null : new String[]{defaultValue}; + return defaultValue == null ? Strings.EMPTY_ARRAY : new String[]{defaultValue}; } else { return property.split(PATHS_SEPARATOR); } @@ -324,11 +325,9 @@ public abstract class ESRestTestCase extends ESIntegTestCase { @Before public void reset() throws IOException, RestException { //skip test if it matches one of the blacklist globs - for (PathMatcher blacklistedPathMatcher : blacklistPathMatchers) { - //we need to replace a few characters otherwise the test section name can't be parsed as a path on windows - String testSection = testCandidate.getTestSection().getName().replace("*", "").replace("\\", "/").replaceAll("\\s+/", "/").replace(":", "").trim(); - String testPath = testCandidate.getSuitePath() + "/" + testSection; - assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher.matches(PathUtils.get(testPath))); + for (BlacklistedPathPatternMatcher blacklistedPathMatcher : blacklistPathMatchers) { + String testPath = testCandidate.getSuitePath() + "/" + testCandidate.getTestSection().getName(); + assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher.isSuffixMatch(testPath)); } //The client needs non static info to get initialized, therefore it can't be initialized in the before class restTestExecutionContext.initClient(cluster().httpAddresses(), restClientSettings()); diff --git a/test-framework/src/test/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcherTests.java b/test-framework/src/test/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcherTests.java new file mode 100644 index 00000000000..9414a2219cf --- /dev/null +++ b/test-framework/src/test/java/org/elasticsearch/test/rest/BlacklistedPathPatternMatcherTests.java @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.test.rest; + + +import org.elasticsearch.test.ESTestCase; + +public class BlacklistedPathPatternMatcherTests extends ESTestCase { + + public void testMatchesExact() { + // suffix match + assertMatch("cat.aliases/10_basic/Empty cluster", "/some/suite_path/cat.aliases/10_basic/Empty cluster"); + // exact match + assertMatch("cat.aliases/10_basic/Empty cluster", "cat.aliases/10_basic/Empty cluster"); + // additional text at the end should not match + assertNoMatch("cat.aliases/10_basic/Empty cluster", "cat.aliases/10_basic/Empty clusters in here"); + } + + public void testMatchesSimpleWildcardPatterns() { + assertMatch("termvector/20_issue7121/*", "/suite/termvector/20_issue7121/test_first"); + assertMatch("termvector/20_issue7121/*", "/suite/termvector/20_issue7121/"); + // do not cross segment boundaries + assertNoMatch("termvector/20_issue7121/*", "/suite/termvector/20_issue7121/test/first"); + } + + public void testMatchesMultiWildcardPatterns() { + assertMatch("indices.get/10_basic/*allow_no_indices*", "/suite/indices.get/10_basic/we_allow_no_indices"); + assertMatch("indices.get/10_basic/*allow_no_indices*", "/suite/indices.get/10_basic/we_allow_no_indices_at_all"); + assertNoMatch("indices.get/10_basic/*allow_no_indices*", "/suite/indices.get/10_basic/we_allow_no_indices_at_all/here"); + assertMatch("indices.get/*/*allow_no_indices*", "/suite/indices.get/10_basic/we_allow_no_indices_at_all"); + assertMatch("indices.get/*/*allow_no_indices*", "/suite/indices.get/20_basic/we_allow_no_indices_at_all"); + assertMatch("*/*/*allow_no_indices*", "/suite/path/to/test/indices.get/20_basic/we_allow_no_indices_at_all"); + } + + public void testMatchesPatternsWithEscapedCommas() { + assertMatch("indices.get/10_basic\\,20_advanced/foo", "/suite/indices.get/10_basic,20_advanced/foo"); + } + + public void testMatchesMixedPatterns() { + assertMatch("indices.get/*/10_basic\\,20_advanced/*foo*", "/suite/indices.get/all/10_basic,20_advanced/foo"); + assertMatch("indices.get/*/10_basic\\,20_advanced/*foo*", "/suite/indices.get/all/10_basic,20_advanced/my_foo"); + assertMatch("indices.get/*/10_basic\\,20_advanced/*foo*", "/suite/indices.get/all/10_basic,20_advanced/foo_bar"); + } + + + + private void assertMatch(String pattern, String path) { + BlacklistedPathPatternMatcher matcher = new BlacklistedPathPatternMatcher(pattern); + assertTrue("Pattern [" + pattern + "] should have matched path [" + path + "]", matcher.isSuffixMatch(path)); + } + + private void assertNoMatch(String pattern, String path) { + BlacklistedPathPatternMatcher matcher = new BlacklistedPathPatternMatcher(pattern); + assertFalse("Pattern [" + pattern + "] should not have matched path [" + path + "]", matcher.isSuffixMatch(path)); + } +} \ No newline at end of file