blob: cf1183310e7a61030e1d0486c7d17f71fe187aeb [file] [log] [blame]
// Copyright 2015 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.skyframe;
import com.google.common.base.Predicate;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.ResolvedTargets;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.TestTargetUtils;
import com.google.devtools.build.lib.skyframe.TestsInSuiteValue.TestsInSuiteKey;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.ValueOrException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
/**
* TestsInSuiteFunction takes a single test_suite target and expands all of the tests it contains,
* possibly recursively.
*/
// TODO(ulfjack): What about test_suite rules that include each other.
final class TestsInSuiteFunction implements SkyFunction {
@Override
public SkyValue compute(SkyKey key, Environment env) throws InterruptedException {
TestsInSuiteKey expansion = (TestsInSuiteKey) key.argument();
ResolvedTargets<Target> result =
computeTestsInSuite(env, expansion.getTestSuite(), expansion.isStrict());
if (env.valuesMissing()) {
return null;
}
return new TestsInSuiteValue(result);
}
/**
* Populates 'result' with all the tests associated with the specified 'testSuite'. Throws an
* exception if any target is missing.
*
* <p>CAUTION! Keep this logic consistent with {@code TestSuite}!
*/
private static ResolvedTargets<Target> computeTestsInSuite(
Environment env, Rule testSuite, boolean strict) throws InterruptedException {
ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
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.
builder.mergeError(getPrerequisites(env, testSuite, "tests", testsAndSuites));
// 1. Add all tests
for (Target test : testsAndSuites) {
if (TargetUtils.isTestRule(test)) {
builder.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.
// TODO(ulfjack): We need to throw to end the process if we happen to be in --nokeep_going,
// but we can't know whether or not we are at this point.
env.getListener().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."));
builder.setError();
}
}
// 2. Add implicit dependencies on tests in same package, if any.
List<Target> implicitTests = new ArrayList<>();
builder.mergeError(getPrerequisites(env, testSuite, "$implicit_tests", implicitTests));
for (Target target : implicitTests) {
// 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)) {
builder.add(target);
}
}
// 3. Filter based on tags, size, env.
filterTests(testSuite, builder);
// 4. Expand all suites recursively.
for (Target suite : testsAndSuites) {
if (TargetUtils.isTestSuiteRule(suite)) {
TestsInSuiteValue value =
(TestsInSuiteValue) env.getValue(TestsInSuiteValue.key(suite, strict));
if (value == null) {
continue;
}
builder.merge(value.getTargets());
}
}
return builder.build();
}
/**
* Adds the set of targets found in the attribute named {@code attrName}, which must be of label
* list type, of the {@code test_suite} rule named {@code testSuite}. Returns true if the method
* found a problem during the lookup process; the actual error message is reported to the
* environment.
*/
private static boolean getPrerequisites(
Environment env, Rule testSuite, String attrName, List<Target> targets)
throws InterruptedException {
List<Label> labels =
NonconfigurableAttributeMapper.of(testSuite).get(attrName, BuildType.LABEL_LIST);
Set<PackageIdentifier> pkgIdentifiers = new LinkedHashSet<>();
for (Label label : labels) {
pkgIdentifiers.add(label.getPackageIdentifier());
}
Map<SkyKey, ValueOrException<BuildFileNotFoundException>> packages = env.getValuesOrThrow(
PackageValue.keys(pkgIdentifiers), BuildFileNotFoundException.class);
if (env.valuesMissing()) {
return false;
}
boolean hasError = false;
Map<PackageIdentifier, Package> packageMap = new HashMap<>();
for (Entry<SkyKey, ValueOrException<BuildFileNotFoundException>> entry : packages.entrySet()) {
try {
packageMap.put(
(PackageIdentifier) entry.getKey().argument(),
((PackageValue) entry.getValue().get()).getPackage());
} catch (BuildFileNotFoundException e) {
env.getListener().handle(Event.error(e.getMessage()));
hasError = true;
}
}
for (Label label : labels) {
Package pkg = packageMap.get(label.getPackageIdentifier());
if (pkg == null) {
continue;
}
try {
targets.add(pkg.getTarget(label.getName()));
} catch (NoSuchTargetException e) {
env.getListener().handle(Event.error(e.getMessage()));
hasError = true;
}
}
return hasError;
}
/**
* Filters 'tests' (by mutation) according to the 'tags' attribute, specifically those that
* match ALL of the tags in tagsAttribute.
*/
private static void filterTests(Rule testSuite, ResolvedTargets.Builder<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 =
TestTargetUtils.sortTagsBySense(tagsAttribute);
final Collection<String> positiveTags = tagLists.first;
final Collection<String> negativeTags = tagLists.second;
tests.filter(new Predicate<Target>() {
@Override
public boolean apply(@Nullable Target input) {
Rule test = (Rule) input;
AttributeMap nonConfigurableAttributes = NonconfigurableAttributeMapper.of(test);
List<String> testTags =
new ArrayList<>(nonConfigurableAttributes.get("tags", Type.STRING_LIST));
testTags.add(nonConfigurableAttributes.get("size", Type.STRING));
return includeTest(testTags, positiveTags, negativeTags);
}
});
}
/**
* 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 if 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;
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
}