// 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.TestsInSuite; 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 { TestsInSuite expansion = (TestsInSuite) 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; } }