| // 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.collect.ImmutableSet; |
| 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.AggregatingAttributeMapper; |
| import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException; |
| import com.google.devtools.build.lib.packages.NoSuchPackageException; |
| import com.google.devtools.build.lib.packages.NoSuchTargetException; |
| 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.TestExpansionValue.TestExpansionKey; |
| 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.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import javax.annotation.Nullable; |
| |
| /** |
| * TestExpansionFunction 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 TestExpansionFunction implements SkyFunction { |
| @Override |
| public SkyValue compute(SkyKey key, Environment env) throws InterruptedException { |
| TestExpansionKey expansion = (TestExpansionKey) key.argument(); |
| SkyKey packageKey = PackageValue.key(expansion.getLabel().getPackageIdentifier()); |
| PackageValue pkg = (PackageValue) env.getValue(packageKey); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| Rule rule = pkg.getPackage().getRule(expansion.getLabel().getName()); |
| ResolvedTargets<Label> result = computeExpandedTests(env, rule, expansion.isStrict()); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| return new TestExpansionValue(result); |
| } |
| |
| private static Set<Label> toLabels(Set<Target> targets) { |
| return targets.stream().map(Target::getLabel).collect(Collectors.toSet()); |
| } |
| |
| /** |
| * Populates 'result' with all the tests associated with the specified 'rule'. Throws an exception |
| * if any target is missing. |
| * |
| * <p>CAUTION! Keep this logic consistent with {@code TestSuite}! |
| */ |
| private static ResolvedTargets<Label> computeExpandedTests( |
| Environment env, Rule rule, boolean strict) throws InterruptedException { |
| Set<Target> result = new HashSet<>(); |
| boolean hasError = false; |
| |
| List<Target> prerequisites = new ArrayList<>(); |
| // Note that prerequisites can contain input file targets; the test_suite rule does not |
| // restrict the set of targets that can appear in tests or suites. |
| hasError |= getPrerequisites(env, rule, "tests", prerequisites); |
| |
| // 1. Add all tests |
| for (Target test : prerequisites) { |
| 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. |
| // 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( |
| rule.getLocation(), |
| "in test_suite rule '" |
| + rule.getLabel() |
| + "': expecting a test or a test_suite rule but '" |
| + test.getLabel() |
| + "' is not one.")); |
| hasError = true; |
| } |
| } |
| |
| // 2. Add implicit dependencies on tests in same package, if any. |
| List<Target> implicitTests = new ArrayList<>(); |
| hasError |= getPrerequisites(env, rule, "$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)) { |
| result.add(target); |
| } |
| } |
| |
| // 3. Filter based on tags, size, env. |
| TestTargetUtils.filterTests(rule, result); |
| |
| // 4. Expand all rules recursively, collecting labels. |
| ResolvedTargets.Builder<Label> labelsBuilder = ResolvedTargets.builder(); |
| // Don't set filtered targets; they would be removed from the containing test suite. |
| labelsBuilder.merge(new ResolvedTargets<>(toLabels(result), ImmutableSet.of(), hasError)); |
| |
| for (Target suite : prerequisites) { |
| if (TargetUtils.isTestSuiteRule(suite)) { |
| TestExpansionValue value = |
| (TestExpansionValue) env.getValue(TestExpansionValue.key(suite, strict)); |
| if (value == null) { |
| continue; |
| } |
| labelsBuilder.merge(value.getLabels()); |
| } |
| } |
| |
| return labelsBuilder.build(); |
| } |
| |
| /** |
| * Adds the set of targets found in the attribute named {@code attrName}, which must be of label |
| * or 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 rule, String attrName, List<Target> targets) |
| throws InterruptedException { |
| AggregatingAttributeMapper mapper = AggregatingAttributeMapper.of(rule); |
| List<Label> labels = |
| mapper.visitLabels(mapper.getAttributeDefinition(attrName)).stream() |
| .map(e -> e.getLabel()) |
| .collect(Collectors.toList()); |
| |
| Set<PackageIdentifier> pkgIdentifiers = new LinkedHashSet<>(); |
| for (Label label : labels) { |
| pkgIdentifiers.add(label.getPackageIdentifier()); |
| } |
| |
| Map<SkyKey, ValueOrException<NoSuchPackageException>> packages = |
| env.getValuesOrThrow(PackageValue.keys(pkgIdentifiers), NoSuchPackageException.class); |
| if (env.valuesMissing()) { |
| return false; |
| } |
| boolean hasError = false; |
| Map<PackageIdentifier, Package> packageMap = new HashMap<>(); |
| for (Map.Entry<SkyKey, ValueOrException<NoSuchPackageException>> entry : packages.entrySet()) { |
| try { |
| packageMap.put( |
| (PackageIdentifier) entry.getKey().argument(), |
| ((PackageValue) entry.getValue().get()).getPackage()); |
| } catch (NoSuchPackageException e) { |
| env.getListener().handle(Event.error(e.getMessage())); |
| hasError = true; |
| } |
| } |
| |
| for (Label label : labels) { |
| Package pkg = packageMap.get(label.getPackageIdentifier()); |
| if (pkg == null) { |
| continue; |
| } |
| if (pkg.containsErrors()) { |
| hasError = true; |
| // Abort the build if --nokeep_going. |
| try { |
| env.getValueOrThrow( |
| PackageErrorFunction.key(label.getPackageIdentifier()), |
| BuildFileContainsErrorsException.class); |
| return false; |
| } catch (BuildFileContainsErrorsException e) { |
| // PackageErrorFunction always throws this exception, and this fact is used by Skyframe to |
| // abort the build. If we get here, it's either because of error bubbling or because we're |
| // in --keep_going mode. In either case, we *should* ignore the exception. |
| } |
| } |
| try { |
| targets.add(pkg.getTarget(label.getName())); |
| } catch (NoSuchTargetException e) { |
| env.getListener().handle(Event.error(e.getMessage())); |
| hasError = true; |
| } |
| } |
| return hasError; |
| } |
| |
| @Nullable |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| } |