| // 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.query2.engine; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.MutableMap; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryTaskFuture; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting; |
| import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ThreadSafeMutableSet; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * A tests(x) filter expression, which returns all the tests in set x, expanding test_suite rules |
| * into their constituents. |
| * |
| * <p>Unfortunately this class reproduces a substantial amount of logic from {@code |
| * TestSuiteConfiguredTarget}, albeit in a somewhat simplified form. This is basically inevitable |
| * since the expansion of test_suites cannot be done during the loading phase, because it involves |
| * inter-package references. We make no attempt to validate the input, or report errors or warnings |
| * other than missing target. |
| * |
| * <pre>expr ::= TESTS '(' expr ')'</pre> |
| */ |
| public class TestsFunction implements QueryFunction { |
| @VisibleForTesting |
| public TestsFunction() {} |
| |
| @Override |
| public String getName() { |
| return "tests"; |
| } |
| |
| @Override |
| public int getMandatoryArguments() { |
| return 1; |
| } |
| |
| @Override |
| public List<ArgumentType> getArgumentTypes() { |
| return ImmutableList.of(ArgumentType.EXPRESSION); |
| } |
| |
| @Override |
| public <T> QueryTaskFuture<Void> eval( |
| final QueryEnvironment<T> env, |
| QueryExpressionContext<T> context, |
| QueryExpression expression, |
| List<Argument> args, |
| final Callback<T> callback) { |
| final Closure<T> closure = new Closure<>(expression, env); |
| final Uniquifier<T> uniquifier = env.createUniquifier(); |
| |
| return env.eval( |
| args.get(0).getExpression(), |
| context, |
| new Callback<T>() { |
| @Override |
| public void process(Iterable<T> partialResult) |
| throws QueryException, InterruptedException { |
| for (T target : partialResult) { |
| if (env.getAccessor().isTestRule(target)) { |
| if (uniquifier.unique(target)) { |
| callback.process(ImmutableList.of(target)); |
| } |
| } else if (env.getAccessor().isTestSuite(target)) { |
| for (T test : closure.getTestsInSuite(target)) { |
| T testTarget = env.getOrCreate(test); |
| if (uniquifier.unique(testTarget)) { |
| callback.process(ImmutableList.of(testTarget)); |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| // TODO(ulfjack): This must match the code in TestTargetUtils. However, we don't currently want |
| // to depend on the packages library. Extract to a neutral place? |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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. |
| * |
| * Keep this function in sync with the version in |
| * java.com.google.devtools.build.lib.view.packages.TestTargetUtils.sortTagsBySense |
| * |
| * @param tagList A collection of text tags to separate. |
| */ |
| private static void sortTagsBySense( |
| Collection<String> tagList, Set<String> requiredTags, Set<String> excludedTags) { |
| 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); |
| } |
| } |
| } |
| |
| /** A closure over the temporary state needed to compute the expression. */ |
| @ThreadSafe |
| private static final class Closure<T> { |
| private final QueryExpression expression; |
| /** A dynamically-populated mapping from test_suite rules to their tests. */ |
| private final MutableMap<T, ThreadSafeMutableSet<T>> testsInSuite; |
| |
| /** The environment in which this query is being evaluated. */ |
| private final QueryEnvironment<T> env; |
| |
| private final boolean strict; |
| |
| private Closure(QueryExpression expression, QueryEnvironment<T> env) { |
| this.expression = expression; |
| this.env = env; |
| this.strict = env.isSettingEnabled(Setting.TESTS_EXPRESSION_STRICT); |
| this.testsInSuite = env.createMutableMap(); |
| } |
| |
| /** |
| * Computes and returns the set of test rules in a particular suite. Uses dynamic |
| * programming---a memoized version of {@link #computeTestsInSuite}. |
| * |
| * @precondition env.getAccessor().isTestSuite(testSuite) |
| */ |
| private synchronized ThreadSafeMutableSet<T> getTestsInSuite(T testSuite) |
| throws QueryException, InterruptedException { |
| ThreadSafeMutableSet<T> tests = testsInSuite.get(testSuite); |
| if (tests == null) { |
| tests = env.createThreadSafeMutableSet(); |
| 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. |
| * |
| * <p>CAUTION! Keep this logic consistent with {@code TestsSuiteConfiguredTarget}! |
| * |
| * @precondition env.getAccessor().isTestSuite(testSuite) |
| */ |
| private void computeTestsInSuite(T testSuite, ThreadSafeMutableSet<T> result) |
| throws QueryException, InterruptedException { |
| List<T> 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 (T test : testsAndSuites) { |
| if (env.getAccessor().isTestRule(test)) { |
| result.add(test); |
| } else if (strict && !env.getAccessor().isTestSuite(test)) { |
| // If strict mode is enabled, then give an error for any non-test, non-test-suite targets. |
| env.reportBuildFileError(expression, "The label '" |
| + env.getAccessor().getLabel(test) + "' in the test_suite '" |
| + env.getAccessor().getLabel(testSuite) + "' does not refer to a test or test_suite " |
| + "rule!"); |
| } |
| } |
| |
| // 2. Add implicit dependencies on tests in same package, if any. |
| for (T 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 (env.getAccessor().isTestRule(target)) { |
| result.add(target); |
| } |
| } |
| |
| // 3. Filter based on tags, size, env. |
| filterTests(testSuite, result); |
| |
| // 4. Expand all suites recursively. |
| for (T suite : testsAndSuites) { |
| if (env.getAccessor().isTestSuite(suite)) { |
| result.addAll(getTestsInSuite(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). |
| * |
| * @precondition env.getAccessor().isTestSuite(testSuite) |
| */ |
| private List<T> getPrerequisites(T testSuite, String attrName) |
| throws QueryException, InterruptedException { |
| return env.getAccessor().getLabelListAttr(expression, testSuite, attrName, |
| "couldn't expand '" + attrName |
| + "' attribute of test_suite " + env.getAccessor().getLabel(testSuite) + ": "); |
| } |
| |
| /** |
| * 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(T testSuite, ThreadSafeMutableSet<T> tests) { |
| List<String> tagsAttribute = env.getAccessor().getStringListAttr(testSuite, "tags"); |
| // Split the tags list into positive and negative tags |
| Set<String> requiredTags = new HashSet<>(); |
| Set<String> excludedTags = new HashSet<>(); |
| sortTagsBySense(tagsAttribute, requiredTags, excludedTags); |
| |
| Iterator<T> it = tests.iterator(); |
| while (it.hasNext()) { |
| T test = it.next(); |
| // TODO(ulfjack): This does not match the code used for TestSuite. |
| List<String> testTags = new ArrayList<>(env.getAccessor().getStringListAttr(test, "tags")); |
| testTags.add(env.getAccessor().getStringAttr(test, "size")); |
| if (!includeTest(testTags, requiredTags, excludedTags)) { |
| it.remove(); |
| } |
| } |
| } |
| } |
| } |