blob: 7461e861bf6b8bb6d397302762842b99c27d5535 [file] [log] [blame]
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.packages;
15
16import com.google.common.base.Predicate;
17import com.google.common.collect.ImmutableList;
18import com.google.common.collect.Sets;
19import com.google.devtools.build.lib.cmdline.ResolvedTargets;
20import com.google.devtools.build.lib.cmdline.TargetParsingException;
21import com.google.devtools.build.lib.events.Event;
22import com.google.devtools.build.lib.events.EventHandler;
23import com.google.devtools.build.lib.pkgcache.TargetProvider;
24import com.google.devtools.build.lib.syntax.Label;
Lukacs Berkiffa73ad2015-09-18 11:40:12 +000025import com.google.devtools.build.lib.syntax.Type;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010026import com.google.devtools.build.lib.util.Pair;
27
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.Iterator;
33import java.util.List;
34import java.util.Map;
35import java.util.Set;
36
37/**
38 * Utility functions over test Targets that don't really belong in the base {@link Target}
39 * interface.
40 */
41public final class TestTargetUtils {
42 /**
43 * Returns a predicate to be used for test size filtering, i.e., that only accepts tests of the
44 * given size.
45 */
46 public static Predicate<Target> testSizeFilter(final Set<TestSize> allowedSizes) {
47 return new Predicate<Target>() {
48 @Override
49 public boolean apply(Target target) {
50 if (!(target instanceof Rule)) {
51 return false;
52 }
53 return allowedSizes.contains(TestSize.getTestSize((Rule) target));
54 }
55 };
56 }
57
58 /**
59 * Returns a predicate to be used for test timeout filtering, i.e., that only accepts tests of
60 * the given timeout.
61 **/
62 public static Predicate<Target> testTimeoutFilter(final Set<TestTimeout> allowedTimeouts) {
63 return new Predicate<Target>() {
64 @Override
65 public boolean apply(Target target) {
66 if (!(target instanceof Rule)) {
67 return false;
68 }
69 return allowedTimeouts.contains(TestTimeout.getTestTimeout((Rule) target));
70 }
71 };
72 }
73
74 /**
75 * Returns a predicate to be used for test language filtering, i.e., that only accepts tests of
76 * the specified languages. The reporter and the list of rule names are only used to warn about
77 * unknown languages.
78 */
79 public static Predicate<Target> testLangFilter(List<String> langFilterList,
80 EventHandler reporter, Set<String> allRuleNames) {
81 final Set<String> requiredLangs = new HashSet<>();
82 final Set<String> excludedLangs = new HashSet<>();
83
84 for (String lang : langFilterList) {
85 if (lang.startsWith("-")) {
86 lang = lang.substring(1);
87 excludedLangs.add(lang);
88 } else {
89 requiredLangs.add(lang);
90 }
91 if (!allRuleNames.contains(lang + "_test")) {
92 reporter.handle(
93 Event.warn("Unknown language '" + lang + "' in --test_lang_filters option"));
94 }
95 }
96
97 return new Predicate<Target>() {
98 @Override
99 public boolean apply(Target rule) {
100 String ruleLang = TargetUtils.getRuleLanguage(rule);
101 return (requiredLangs.isEmpty() || requiredLangs.contains(ruleLang))
102 && !excludedLangs.contains(ruleLang);
103 }
104 };
105 }
106
107 /**
108 * Returns whether a test with the specified tags matches a filter (as specified by the set
109 * of its positive and its negative filters).
110 */
111 public static boolean testMatchesFilters(Collection<String> testTags,
112 Collection<String> requiredTags, Collection<String> excludedTags,
113 boolean mustMatchAllPositive) {
114
115 for (String tag : excludedTags) {
116 if (testTags.contains(tag)) {
117 return false;
118 }
119 }
120
121 // Check required tags, if there are any.
122 if (!requiredTags.isEmpty()) {
123 if (mustMatchAllPositive) {
124 // Require all tags to be present.
125 for (String tag : requiredTags) {
126 if (!testTags.contains(tag)) {
127 return false;
128 }
129 }
130 return true;
131 } else {
132 // Require at least one positive tag.
133 for (String tag : requiredTags) {
134 if (testTags.contains(tag)) {
135 return true;
136 }
137 }
138 }
139
140 return false; // No positive tag found.
141 }
142
143 return true; // No tags are required.
144 }
145
146 /**
147 * Returns a predicate to be used for test tag filtering, i.e., that only accepts tests that match
148 * all of the required tags and none of the excluded tags.
149 */
150 // TODO(bazel-team): This also applies to non-test rules, so should probably be moved to
151 // TargetUtils.
152 public static Predicate<Target> tagFilter(List<String> tagFilterList) {
153 Pair<Collection<String>, Collection<String>> tagLists = sortTagsBySense(tagFilterList);
154 final Collection<String> requiredTags = tagLists.first;
155 final Collection<String> excludedTags = tagLists.second;
156 return new Predicate<Target>() {
157 @Override
158 public boolean apply(Target input) {
159 if (!(input instanceof Rule)) {
160 return false;
161 }
162 // Note that test_tags are those originating from the XX_test rule,
163 // whereas the requiredTags and excludedTags originate from the command
164 // line or test_suite rule.
165 return testMatchesFilters(((Rule) input).getRuleTags(),
166 requiredTags, excludedTags, false);
167 }
168 };
169 }
170
171 /**
172 * Separates a list of text "tags" into a Pair of Collections, where
173 * the first element are the required or positive tags and the second element
174 * are the excluded or negative tags.
175 * This should work on tag list provided from the command line
176 * --test_tags_filters flag or on tag filters explicitly declared in the
177 * suite.
178 *
179 * @param tagList A collection of text targets to separate.
180 */
181 public static Pair<Collection<String>, Collection<String>> sortTagsBySense(
182 Iterable<String> tagList) {
183 Collection<String> requiredTags = new HashSet<>();
184 Collection<String> excludedTags = new HashSet<>();
185
186 for (String tag : tagList) {
187 if (tag.startsWith("-")) {
188 excludedTags.add(tag.substring(1));
189 } else if (tag.startsWith("+")) {
190 requiredTags.add(tag.substring(1));
191 } else if (tag.equals("manual")) {
192 // Ignore manual attribute because it is an exception: it is not a filter
193 // but a property of test_suite
194 continue;
195 } else {
196 requiredTags.add(tag);
197 }
198 }
199 return Pair.of(requiredTags, excludedTags);
200 }
201
202 /**
203 * Returns the (new, mutable) set of test rules, expanding all 'test_suite' rules into the
204 * individual tests they group together and preserving other test target instances.
205 *
206 * Method assumes that passed collection contains only *_test and test_suite rules. While, at this
207 * point it will successfully preserve non-test rules as well, there is no guarantee that this
208 * behavior will be kept in the future.
209 *
210 * @param targetProvider a target provider
211 * @param eventHandler a failure eventHandler to report loading failures to
212 * @param targets Collection of the *_test and test_suite configured targets
213 * @return a duplicate-free iterable of the tests under the specified targets
214 */
215 public static ResolvedTargets<Target> expandTestSuites(TargetProvider targetProvider,
216 EventHandler eventHandler, Iterable<? extends Target> targets, boolean strict,
217 boolean keepGoing)
218 throws TargetParsingException {
219 Closure closure = new Closure(targetProvider, eventHandler, strict, keepGoing);
220 ResolvedTargets.Builder<Target> result = ResolvedTargets.builder();
221 for (Target target : targets) {
222 if (TargetUtils.isTestRule(target)) {
223 result.add(target);
224 } else if (TargetUtils.isTestSuiteRule(target)) {
225 result.addAll(closure.getTestsInSuite((Rule) target));
226 } else {
227 result.add(target);
228 }
229 }
230 if (closure.hasError) {
231 result.setError();
232 }
233 return result.build();
234 }
235
236 // TODO(bazel-team): This is a copy of TestsExpression.Closure with some minor changes; this
237 // should be unified.
238 private static final class Closure {
239 private final TargetProvider targetProvider;
240
241 private final EventHandler eventHandler;
242
243 private final boolean keepGoing;
244
245 private final boolean strict;
246
247 private final Map<Target, Set<Target>> testsInSuite = new HashMap<>();
248
249 private boolean hasError;
250
251 public Closure(TargetProvider targetProvider, EventHandler eventHandler, boolean strict,
252 boolean keepGoing) {
253 this.targetProvider = targetProvider;
254 this.eventHandler = eventHandler;
255 this.strict = strict;
256 this.keepGoing = keepGoing;
257 }
258
259 /**
260 * Computes and returns the set of test rules in a particular suite. Uses
261 * dynamic programming---a memoized version of {@link #computeTestsInSuite}.
262 */
263 private Set<Target> getTestsInSuite(Rule testSuite) throws TargetParsingException {
264 Set<Target> tests = testsInSuite.get(testSuite);
265 if (tests == null) {
266 tests = Sets.newHashSet();
267 testsInSuite.put(testSuite, tests); // break cycles by inserting empty set early.
268 computeTestsInSuite(testSuite, tests);
269 }
270 return tests;
271 }
272
273 /**
274 * Populates 'result' with all the tests associated with the specified
275 * 'testSuite'. Throws an exception if any target is missing.
276 *
277 * CAUTION! Keep this logic consistent with {@code TestsSuiteConfiguredTarget}!
278 */
279 private void computeTestsInSuite(Rule testSuite, Set<Target> result)
280 throws TargetParsingException {
281 List<Target> testsAndSuites = new ArrayList<>();
282 // Note that testsAndSuites can contain input file targets; the test_suite rule does not
283 // restrict the set of targets that can appear in tests or suites.
284 testsAndSuites.addAll(getPrerequisites(testSuite, "tests"));
Lukacs Berkiffa73ad2015-09-18 11:40:12 +0000285 if (testSuite.getRuleClassObject().hasAttr("suites", BuildType.LABEL_LIST)) {
Googlerda649742015-03-12 10:28:48 +0000286 testsAndSuites.addAll(getPrerequisites(testSuite, "suites"));
287 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100288
289 // 1. Add all tests
290 for (Target test : testsAndSuites) {
291 if (TargetUtils.isTestRule(test)) {
292 result.add(test);
293 } else if (strict && !TargetUtils.isTestSuiteRule(test)) {
294 // If strict mode is enabled, then give an error for any non-test, non-test-suite targets.
295 eventHandler.handle(Event.error(testSuite.getLocation(),
296 "in test_suite rule '" + testSuite.getLabel()
297 + "': expecting a test or a test_suite rule but '" + test.getLabel()
298 + "' is not one."));
299 hasError = true;
300 if (!keepGoing) {
301 throw new TargetParsingException("Test suite expansion failed.");
302 }
303 }
304 }
305
306 // 2. Add implicit dependencies on tests in same package, if any.
307 for (Target target : getPrerequisites(testSuite, "$implicit_tests")) {
308 // The Package construction of $implicit_tests ensures that this check never fails, but we
309 // add it here anyway for compatibility with future code.
310 if (TargetUtils.isTestRule(target)) {
311 result.add(target);
312 }
313 }
314
315 // 3. Filter based on tags, size, env.
316 filterTests(testSuite, result);
317
318 // 4. Expand all suites recursively.
319 for (Target suite : testsAndSuites) {
320 if (TargetUtils.isTestSuiteRule(suite)) {
321 result.addAll(getTestsInSuite((Rule) suite));
322 }
323 }
324 }
325
326 /**
327 * Returns the set of rules named by the attribute 'attrName' of test_suite rule 'testSuite'.
328 * The attribute must be a list of labels. If a target cannot be resolved, then an error is
329 * reported to the environment (which may throw an exception if {@code keep_going} is disabled).
330 */
331 private Collection<Target> getPrerequisites(Rule testSuite, String attrName)
332 throws TargetParsingException {
333 try {
334 List<Target> targets = new ArrayList<>();
335 // TODO(bazel-team): This serializes package loading in some cases. We might want to make
336 // this multi-threaded.
337 for (Label label :
Lukacs Berkiffa73ad2015-09-18 11:40:12 +0000338 NonconfigurableAttributeMapper.of(testSuite).get(attrName, BuildType.LABEL_LIST)) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100339 targets.add(targetProvider.getTarget(eventHandler, label));
340 }
341 return targets;
342 } catch (NoSuchThingException e) {
343 if (keepGoing) {
344 hasError = true;
345 eventHandler.handle(Event.error(e.getMessage()));
346 return ImmutableList.of();
347 }
348 throw new TargetParsingException(e.getMessage(), e);
349 } catch (InterruptedException e) {
350 Thread.currentThread().interrupt();
351 throw new TargetParsingException("interrupted", e);
352 }
353 }
354
355 /**
356 * Filters 'tests' (by mutation) according to the 'tags' attribute, specifically those that
357 * match ALL of the tags in tagsAttribute.
358 *
359 * @precondition {@code env.getAccessor().isTestSuite(testSuite)}
360 * @precondition {@code env.getAccessor().isTestRule(test)} for all test in tests
361 */
362 private void filterTests(Rule testSuite, Set<Target> tests) {
363 List<String> tagsAttribute =
364 NonconfigurableAttributeMapper.of(testSuite).get("tags", Type.STRING_LIST);
365 // Split the tags list into positive and negative tags
366 Pair<Collection<String>, Collection<String>> tagLists = sortTagsBySense(tagsAttribute);
367 Collection<String> positiveTags = tagLists.first;
368 Collection<String> negativeTags = tagLists.second;
369
370 Iterator<Target> it = tests.iterator();
371 while (it.hasNext()) {
372 Rule test = (Rule) it.next();
373 AttributeMap nonConfigurableAttributes = NonconfigurableAttributeMapper.of(test);
374 List<String> testTags =
375 new ArrayList<>(nonConfigurableAttributes.get("tags", Type.STRING_LIST));
376 testTags.add(nonConfigurableAttributes.get("size", Type.STRING));
377 if (!includeTest(testTags, positiveTags, negativeTags)) {
378 it.remove();
379 }
380 }
381 }
382
383 /**
384 * Decides whether to include a test in a test_suite or not.
385 * @param testTags Collection of all tags exhibited by a given test.
386 * @param positiveTags Tags declared by the suite. A Test must match ALL of these.
387 * @param negativeTags Tags declared by the suite. A Test must match NONE of these.
388 * @return false is the test is to be removed.
389 */
390 private static boolean includeTest(Collection<String> testTags,
391 Collection<String> positiveTags, Collection<String> negativeTags) {
392 // Add this test if it matches ALL of the positive tags and NONE of the
393 // negative tags in the tags attribute.
394 for (String tag : negativeTags) {
395 if (testTags.contains(tag)) {
396 return false;
397 }
398 }
399 for (String tag : positiveTags) {
400 if (!testTags.contains(tag)) {
401 return false;
402 }
403 }
404 return true;
405 }
406 }
407}