/*
* Copyright 2013-present Facebook, Inc.
*
* Licensed 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 com.facebook.buck.test.selectors;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
/**
* A collection of {@link PatternTestSelector} instances which, as a group, can decide whether or
* not to include a given {@link TestDescription}.
*/
public class TestSelectorList {
public static final TestSelectorList EMPTY = TestSelectorList.builder().build();
/**
* Test selector strings are parsed in two places: (i) by "buck test" when it is first run, to
* validate that the selectors make sense; and (ii) by the JUnitStep's JUnitRunner, which is what
* actually does the test selecting.
*
* <p>We keep a list of the raw selectors used to build out List of TestSelectors so that they can
* be passed from (i) to (ii). This is expensive in that it wastes time re-parsing selectors, but
* it means that if the input is an "@/tmp/long-list-of-tests.txt" then re-using that terse
* argument keeps the "JUnitSteps" Junit java command line nice and short.
*/
private final List<TestSelector> testSelectors;
private final boolean defaultIsInclusive;
private TestSelectorList(List<TestSelector> testSelectors, boolean defaultIsInclusive) {
this.testSelectors = testSelectors;
this.defaultIsInclusive = defaultIsInclusive;
}
private TestSelector defaultSelector() {
return defaultIsInclusive
? PatternTestSelector.INCLUDE_EVERYTHING
: PatternTestSelector.EXCLUDE_EVERYTHING;
}
public TestSelector findSelector(TestDescription description) {
for (TestSelector testSelector : testSelectors) {
if (testSelector.matches(description)) {
return testSelector;
}
}
return defaultSelector();
}
public boolean isIncluded(TestDescription description) {
return findSelector(description).isInclusive();
}
/**
* Returns true if it is *possible* for the given classname to include tests.
*
* <p>Before we go through the hassle of loading a class, confirm that it's possible for it to run
* tests.
*/
public boolean possiblyIncludesClassName(String className) {
for (TestSelector testSelector : testSelectors) {
if (testSelector.matchesClassName(className)) {
if (testSelector.isInclusive()) {
return true;
}
if (testSelector.isMatchAnyMethod()) {
return false;
}
}
}
return defaultSelector().isInclusive();
}
public List<String> getExplanation() {
List<String> lines = new ArrayList<>();
for (TestSelector testSelector : testSelectors) {
lines.add(testSelector.getExplanation());
}
lines.add(defaultSelector().getExplanation());
return lines;
}
public List<String> getRawSelectors() {
List<String> rawSelectors = new ArrayList<>();
for (TestSelector testSelector : testSelectors) {
String rawSelector = testSelector.getRawSelector();
rawSelectors.add(rawSelector);
}
return rawSelectors;
}
public boolean isEmpty() {
return testSelectors.isEmpty();
}
public static TestSelectorList empty() {
return EMPTY;
}
public static Builder builder() {
return new Builder();
}
/**
* Build a new {@link TestSelectorList} from a list of strings, each of which is parsed by {@link
* PatternTestSelector}.
*
* <p>If any of the selectors is an inclusive selector, everything else will be excluded.
* Conversely, if all of the selectors are exclusive, then everything else will be included by
* default.
*/
public static class Builder {
private final List<TestSelector> testSelectors;
protected Builder() {
testSelectors = new ArrayList<>();
}
private Builder addRawSelector(String rawSelector) {
if (rawSelector.charAt(0) == '@') {
try {
String pathString = rawSelector.substring(1);
if (pathString.isEmpty()) {
throw new TestSelectorParseException("Doesn't mention a path!");
}
File file = new File(pathString);
loadFromFile(file);
} catch (TestSelectorParseException | IOException e) {
String message =
String.format("Error with test-selector '%s': %s", rawSelector, e.getMessage());
throw new RuntimeException(message, e);
}
return this;
} else {
TestSelector testSelector = PatternTestSelector.buildFromSelectorString(rawSelector);
this.testSelectors.add(testSelector);
}
return this;
}
public Builder addRawSelectors(String... rawSelectors) {
return addRawSelectors(Arrays.asList(rawSelectors));
}
public Builder addRawSelectors(Collection<String> rawTestSelectors) {
rawTestSelectors.forEach(this::addRawSelector);
return this;
}
public Builder addSimpleTestSelector(String simpleSelector) {
String[] selectorParts = simpleSelector.split(",");
if (selectorParts.length != 2) {
throw new IllegalArgumentException();
}
String className = selectorParts[0];
String methodName = selectorParts[1];
this.testSelectors.add(new SimpleTestSelector(className, methodName));
return this;
}
public Builder addBase64EncodedTestSelector(String b64Selector) {
String[] selectorParts = b64Selector.split(",");
if (selectorParts.length != 2) {
throw new IllegalArgumentException();
}
String className = selectorParts[0];
String methodName = selectorParts[1];
Base64.Decoder decoder = Base64.getDecoder();
try {
String decodedClassName = new String(decoder.decode(className), "UTF-8");
String decodedMethodName = new String(decoder.decode(methodName), "UTF-8");
this.testSelectors.add(new SimpleTestSelector(decodedClassName, decodedMethodName));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return this;
}
Builder loadFromFile(File file) throws IOException {
try (FileReader tempReader = new FileReader(file);
BufferedReader in = new BufferedReader(tempReader)) {
String line;
int lineNumber = 1;
while ((line = in.readLine()) != null) {
try {
addRawSelector(line.trim());
lineNumber++;
} catch (TestSelectorParseException e) {
String message =
String.format("Test selector error in %s at line %d", file, lineNumber);
throw new TestSelectorParseException(message, e);
}
}
} catch (IOException e) {
throw e;
}
return this;
}
public TestSelectorList build() {
boolean defaultIsInclusive = true;
List<TestSelector> selectorsToUse = new ArrayList<>();
for (TestSelector testSelector : testSelectors) {
// Default to being inclusive only if all selectors are *exclusive*.
defaultIsInclusive = defaultIsInclusive && !testSelector.isInclusive();
// If a selector is universal (matches every class and method), no need to look further
if (testSelector.isMatchAnyClass() && testSelector.isMatchAnyMethod()) {
defaultIsInclusive = testSelector.isInclusive();
break;
}
selectorsToUse.add(testSelector);
}
return new TestSelectorList(selectorsToUse, defaultIsInclusive);
}
}
}