aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/buildtool/InstrumentationFilterSupport.java
blob: 5781e0272b113b7a8119b8e62d7f5bcdc41b47ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Copyright 2017 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.buildtool;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.NonconfigurableAttributeMapper;
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.vfs.PathFragment;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedSet;

/**
 * Helper class to heuristically compute an instrumentation filter from a list of tests to run.
 */
public final class InstrumentationFilterSupport {
  public static final String INSTRUMENTATION_FILTER_FLAG = "instrumentation_filter";

  /**
   * Method implements a heuristic used to set default value of the
   * --instrumentation_filter option. Following algorithm is used:
   * 1) Identify all test targets on the command line.
   * 2) Expand all test suites into the individual test targets
   * 3) Calculate list of package names containing all test targets above.
   * 4) Replace all "javatests/" substrings in package names with "java/".
   * 5) If two packages reside in the same directory, use filter based on
   *    the parent directory name instead. Doing so significantly simplifies
   *    instrumentation filter in majority of real-life scenarios (in
   *    particular when dealing with my/package/... wildcards).
   * 6) Set --instrumentation_filter default value to instrument everything
   *    in those packages.
   */
  @VisibleForTesting
  public static String computeInstrumentationFilter(
      EventHandler eventHandler, Collection<Target> testTargets) {
    SortedSet<String> packageFilters = Sets.newTreeSet();
    collectInstrumentedPackages(testTargets, packageFilters);
    optimizeFilterSet(packageFilters);

    String instrumentationFilter = "//" + Joiner.on(",//").join(packageFilters);
    if (!packageFilters.isEmpty()) {
      eventHandler.handle(
          Event.info("Using default value for --instrumentation_filter: \""
              + instrumentationFilter + "\"."));
      eventHandler.handle(Event.info("Override the above default with --"
          + INSTRUMENTATION_FILTER_FLAG));
    }
    return instrumentationFilter;
  }

  private static void collectInstrumentedPackages(
      Collection<Target> targets, Set<String> packageFilters) {
    for (Target target : targets) {
      // Add package-based filters for every test target.
      String prefix = getInstrumentedPrefix(target.getLabel().getPackageName());
      if (!prefix.isEmpty()) {
        packageFilters.add(prefix);
      }
      if (TargetUtils.isTestSuiteRule(target)) {
        AttributeMap attributes = NonconfigurableAttributeMapper.of((Rule) target);
        // We don't need to handle $implicit_tests attribute since we already added
        // test_suite package to the set.
        for (Label label : attributes.get("tests", BuildType.LABEL_LIST)) {
          // Add package-based filters for all tests in the test suite.
          packageFilters.add(getInstrumentedPrefix(label.getPackageName()));
        }
      }
    }
  }

  /**
   * Returns prefix string that should be instrumented for a given package. Input string should
   * be formatted like the output of Label.getPackageName().
   * Generally, package name will be used as such string with two modifications.
   * - "javatests/ directories will be substituted with "java/", since we do
   * not want to instrument java test code. "java/" directories in "test/" will
   * be replaced by the same in "main/".
   * - "/internal", "/public", and "tests/" package suffix will be dropped, since usually we would
   * want to instrument code in the parent package as well
   */
  @VisibleForTesting
  public static String getInstrumentedPrefix(String packageName) {
    if (packageName.endsWith("/internal")) {
      packageName = packageName.substring(0, packageName.length() - "/internal".length());
    } else if (packageName.endsWith("/public")) {
      packageName = packageName.substring(0, packageName.length() - "/public".length());
    } else if (packageName.endsWith("/tests")) {
      packageName = packageName.substring(0, packageName.length() - "/tests".length());
    }
    return packageName
        .replaceFirst("(?<=^|/)javatests/", "java/")
        .replaceFirst("(?<=^|/)test/java/", "main/java/");
  }

  private static void optimizeFilterSet(SortedSet<String> packageFilters) {
    Iterator<String> iterator = packageFilters.iterator();
    if (iterator.hasNext()) {
      // Find common parent filters to reduce number of filter expressions. In practice this
      // still produces nicely constrained instrumentation filter while making final
      // filter value much more user-friendly - especially in case of /my/package/... wildcards.
      Set<String> parentFilters = Sets.newTreeSet();
      String filterString = iterator.next();
      PathFragment parent = PathFragment.create(filterString).getParentDirectory();
      while (iterator.hasNext()) {
        String current = iterator.next();
        if (parent != null && parent.getPathString().length() > 0
            && !current.startsWith(filterString) && current.startsWith(parent.getPathString())) {
          parentFilters.add(parent.getPathString());
        } else {
          filterString = current;
          parent = PathFragment.create(filterString).getParentDirectory();
        }
      }
      packageFilters.addAll(parentFilters);

      // Optimize away nested filters.
      iterator = packageFilters.iterator();
      String prev = iterator.next();
      while (iterator.hasNext()) {
        String current = iterator.next();
        if (current.startsWith(prev)) {
          iterator.remove();
        } else {
          prev = current;
        }
      }
    }
  }
}