blob: b51285001d2451b31bfce5fea19c9a27bd9a5163 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.packages;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.ResolvedTargets;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.pkgcache.TargetProvider;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Pair;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility functions over test Targets that don't really belong in the base {@link Target}
* interface.
*/
public final class TestTargetUtils {
/**
* Returns a predicate to be used for test size filtering, i.e., that only accepts tests of the
* given size.
*/
public static Predicate<Target> testSizeFilter(final Set<TestSize> allowedSizes) {
return target -> {
if (!(target instanceof Rule)) {
return false;
}
return allowedSizes.contains(TestSize.getTestSize((Rule) target));
};
}
/**
* Returns a predicate to be used for test timeout filtering, i.e., that only accepts tests of
* the given timeout.
**/
public static Predicate<Target> testTimeoutFilter(final Set<TestTimeout> allowedTimeouts) {
return target -> {
if (!(target instanceof Rule)) {
return false;
}
return allowedTimeouts.contains(TestTimeout.getTestTimeout((Rule) target));
};
}
/**
* Returns a predicate to be used for test language filtering, i.e., that only accepts tests of
* the specified languages. The reporter and the list of rule names are only used to warn about
* unknown languages.
*/
public static Predicate<Target> testLangFilter(
List<String> langFilterList, ExtendedEventHandler reporter, Set<String> allRuleNames) {
final Set<String> requiredLangs = new HashSet<>();
final Set<String> excludedLangs = new HashSet<>();
for (String lang : langFilterList) {
if (lang.startsWith("-")) {
lang = lang.substring(1);
excludedLangs.add(lang);
} else {
requiredLangs.add(lang);
}
if (!allRuleNames.contains(lang + "_test")) {
reporter.handle(
Event.warn("Unknown language '" + lang + "' in --test_lang_filters option"));
}
}
return rule -> {
String ruleLang = TargetUtils.getRuleLanguage(rule);
return (requiredLangs.isEmpty() || requiredLangs.contains(ruleLang))
&& !excludedLangs.contains(ruleLang);
};
}
/**
* Returns whether a test with the specified tags matches a filter (as specified by the set
* of its positive and its negative filters).
*/
public static boolean testMatchesFilters(Collection<String> testTags,
Collection<String> requiredTags, Collection<String> excludedTags,
boolean mustMatchAllPositive) {
for (String tag : excludedTags) {
if (testTags.contains(tag)) {
return false;
}
}
// Check required tags, if there are any.
if (!requiredTags.isEmpty()) {
if (mustMatchAllPositive) {
// Require all tags to be present.
for (String tag : requiredTags) {
if (!testTags.contains(tag)) {
return false;
}
}
return true;
} else {
// Require at least one positive tag.
for (String tag : requiredTags) {
if (testTags.contains(tag)) {
return true;
}
}
}
return false; // No positive tag found.
}
return true; // No tags are required.
}
/**
* Separates a list of text "tags" into a Pair of Collections, where
* the first element are the required or positive tags and the second element
* are the excluded or negative tags.
* This should work on tag list provided from the command line
* --test_tags_filters flag or on tag filters explicitly declared in the
* suite.
*
* @param tagList A collection of text targets to separate.
*/
public static Pair<Collection<String>, Collection<String>> sortTagsBySense(
Iterable<String> tagList) {
Collection<String> requiredTags = new HashSet<>();
Collection<String> excludedTags = new HashSet<>();
for (String tag : tagList) {
if (tag.startsWith("-")) {
excludedTags.add(tag.substring(1));
} else if (tag.startsWith("+")) {
requiredTags.add(tag.substring(1));
} else if (tag.equals("manual")) {
// Ignore manual attribute because it is an exception: it is not a filter
// but a property of test_suite
continue;
} else {
requiredTags.add(tag);
}
}
return Pair.of(requiredTags, excludedTags);
}
/**
* Returns the (new, mutable) set of test rules, expanding all 'test_suite' rules into the
* individual tests they group together and preserving other test target instances.
*
* <p>Method assumes that passed collection contains only *_test and test_suite rules. While, at
* this point it will successfully preserve non-test rules as well, there is no guarantee that
* this behavior will be kept in the future.
*
* @param targetProvider a target provider
* @param eventHandler a failure eventHandler to report loading failures to
* @param targets Collection of the *_test and test_suite configured targets
* @return a duplicate-free iterable of the tests under the specified targets
*/
public static ResolvedTargets<Target> expandTestSuites(
TargetProvider targetProvider,
ExtendedEventHandler eventHandler,
Iterable<? extends Target> targets,
boolean strict,
boolean keepGoing)
throws TargetParsingException {
Closure closure = new Closure(targetProvider, eventHandler, strict, keepGoing);
ResolvedTargets.Builder<Target> result = ResolvedTargets.builder();
for (Target target : targets) {
if (TargetUtils.isTestRule(target)) {
result.add(target);
} else if (TargetUtils.isTestSuiteRule(target)) {
result.addAll(closure.getTestsInSuite((Rule) target));
} else {
result.add(target);
}
}
if (closure.hasError) {
result.setError();
}
return result.build();
}
// TODO(bazel-team): This is a copy of TestsExpression.Closure with some minor changes; this
// should be unified.
private static final class Closure {
private final TargetProvider targetProvider;
private final ExtendedEventHandler eventHandler;
private final boolean keepGoing;
private final boolean strict;
private final Map<Target, Set<Target>> testsInSuite = new HashMap<>();
private boolean hasError;
public Closure(
TargetProvider targetProvider,
ExtendedEventHandler eventHandler,
boolean strict,
boolean keepGoing) {
this.targetProvider = targetProvider;
this.eventHandler = eventHandler;
this.strict = strict;
this.keepGoing = keepGoing;
}
/**
* Computes and returns the set of test rules in a particular suite. Uses
* dynamic programming---a memoized version of {@link #computeTestsInSuite}.
*/
private Set<Target> getTestsInSuite(Rule testSuite) throws TargetParsingException {
Set<Target> tests = testsInSuite.get(testSuite);
if (tests == null) {
tests = Sets.newHashSet();
testsInSuite.put(testSuite, tests); // break cycles by inserting empty set early.
computeTestsInSuite(testSuite, tests);
}
return tests;
}
/**
* Populates 'result' with all the tests associated with the specified
* 'testSuite'. Throws an exception if any target is missing.
*
* CAUTION! Keep this logic consistent with {@code TestsSuiteConfiguredTarget}!
*/
private void computeTestsInSuite(Rule testSuite, Set<Target> result)
throws TargetParsingException {
List<Target> testsAndSuites = new ArrayList<>();
// Note that testsAndSuites can contain input file targets; the test_suite rule does not
// restrict the set of targets that can appear in tests or suites.
testsAndSuites.addAll(getPrerequisites(testSuite, "tests"));
// 1. Add all tests
for (Target test : testsAndSuites) {
if (TargetUtils.isTestRule(test)) {
result.add(test);
} else if (strict && !TargetUtils.isTestSuiteRule(test)) {
// If strict mode is enabled, then give an error for any non-test, non-test-suite targets.
eventHandler.handle(Event.error(testSuite.getLocation(),
"in test_suite rule '" + testSuite.getLabel()
+ "': expecting a test or a test_suite rule but '" + test.getLabel()
+ "' is not one."));
hasError = true;
if (!keepGoing) {
throw new TargetParsingException("Test suite expansion failed.");
}
}
}
// 2. Add implicit dependencies on tests in same package, if any.
for (Target target : getPrerequisites(testSuite, "$implicit_tests")) {
// The Package construction of $implicit_tests ensures that this check never fails, but we
// add it here anyway for compatibility with future code.
if (TargetUtils.isTestRule(target)) {
result.add(target);
}
}
// 3. Filter based on tags, size, env.
filterTests(testSuite, result);
// 4. Expand all suites recursively.
for (Target suite : testsAndSuites) {
if (TargetUtils.isTestSuiteRule(suite)) {
result.addAll(getTestsInSuite((Rule) suite));
}
}
}
/**
* Returns the set of rules named by the attribute 'attrName' of test_suite rule 'testSuite'.
* The attribute must be a list of labels. If a target cannot be resolved, then an error is
* reported to the environment (which may throw an exception if {@code keep_going} is disabled).
*/
private Collection<Target> getPrerequisites(Rule testSuite, String attrName)
throws TargetParsingException {
try {
List<Target> targets = new ArrayList<>();
// TODO(bazel-team): This serializes package loading in some cases. We might want to make
// this multi-threaded.
for (Label label :
NonconfigurableAttributeMapper.of(testSuite).get(attrName, BuildType.LABEL_LIST)) {
targets.add(targetProvider.getTarget(eventHandler, label));
}
return targets;
} catch (NoSuchThingException e) {
if (keepGoing) {
hasError = true;
eventHandler.handle(Event.error(e.getMessage()));
return ImmutableList.of();
}
throw new TargetParsingException(e.getMessage(), e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new TargetParsingException("interrupted", e);
}
}
/**
* Filters 'tests' (by mutation) according to the 'tags' attribute, specifically those that
* match ALL of the tags in tagsAttribute.
*
* @precondition {@code env.getAccessor().isTestSuite(testSuite)}
* @precondition {@code env.getAccessor().isTestRule(test)} for all test in tests
*/
private void filterTests(Rule testSuite, Set<Target> tests) {
List<String> tagsAttribute =
NonconfigurableAttributeMapper.of(testSuite).get("tags", Type.STRING_LIST);
// Split the tags list into positive and negative tags
Pair<Collection<String>, Collection<String>> tagLists = sortTagsBySense(tagsAttribute);
Collection<String> positiveTags = tagLists.first;
Collection<String> negativeTags = tagLists.second;
Iterator<Target> it = tests.iterator();
while (it.hasNext()) {
Rule test = (Rule) it.next();
AttributeMap nonConfigurableAttributes = NonconfigurableAttributeMapper.of(test);
List<String> testTags =
new ArrayList<>(nonConfigurableAttributes.get("tags", Type.STRING_LIST));
testTags.add(nonConfigurableAttributes.get("size", Type.STRING));
if (!includeTest(testTags, positiveTags, negativeTags)) {
it.remove();
}
}
}
/**
* Decides whether to include a test in a test_suite or not.
* @param testTags Collection of all tags exhibited by a given test.
* @param positiveTags Tags declared by the suite. A Test must match ALL of these.
* @param negativeTags Tags declared by the suite. A Test must match NONE of these.
* @return false is the test is to be removed.
*/
private static boolean includeTest(Collection<String> testTags,
Collection<String> positiveTags, Collection<String> negativeTags) {
// Add this test if it matches ALL of the positive tags and NONE of the
// negative tags in the tags attribute.
for (String tag : negativeTags) {
if (testTags.contains(tag)) {
return false;
}
}
for (String tag : positiveTags) {
if (!testTags.contains(tag)) {
return false;
}
}
return true;
}
}
}