• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.util.testmapping;
17 
18 import static com.google.common.base.Preconditions.checkState;
19 
20 import com.android.tradefed.log.LogUtil.CLog;
21 
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.LinkedHashSet;
26 import java.util.List;
27 import java.util.Set;
28 import java.util.stream.Collectors;
29 
30 /** Stores the test information set in a TEST_MAPPING file. */
31 public class TestInfo {
32     private static final String OPTION_INCLUDE_ANNOTATION = "include-annotation";
33     private static final String OPTION_EXCLUDE_ANNOTATION = "exclude-annotation";
34 
35     private String mName = null;
36     private List<TestOption> mOptions = new ArrayList<TestOption>();
37     // A Set of locations with TEST_MAPPING files that containing the test.
38     private Set<String> mSources = new HashSet<String>();
39     // A Set of import paths defined in TEST_MAPPING files.
40     private Set<String> mImportPaths = new HashSet<String>();
41     // True if the test should run on host and require no device.
42     private boolean mHostOnly = false;
43     // A Set of keywords to be matched when filtering tests to run in a Test Mapping suite.
44     private Set<String> mKeywords = null;
45 
TestInfo(String name, String source, boolean hostOnly)46     public TestInfo(String name, String source, boolean hostOnly) {
47         this(name, source, hostOnly, new HashSet<String>());
48     }
49 
TestInfo(String name, String source, boolean hostOnly, Set<String> keywords)50     public TestInfo(String name, String source, boolean hostOnly, Set<String> keywords) {
51         mName = name;
52         mSources.add(source);
53         mHostOnly = hostOnly;
54         mKeywords = keywords;
55     }
56 
getName()57     public String getName() {
58         return mName;
59     }
60 
addOption(TestOption option)61     public void addOption(TestOption option) {
62         mOptions.add(option);
63         Collections.sort(mOptions);
64     }
65 
getOptions()66     public List<TestOption> getOptions() {
67         return mOptions;
68     }
69 
addSources(Set<String> sources)70     public void addSources(Set<String> sources) {
71         mSources.addAll(sources);
72     }
73 
addImportPaths(Set<String> paths)74     public void addImportPaths(Set<String> paths) {
75         mImportPaths.addAll(paths);
76     }
77 
getImportPaths()78     public Set<String> getImportPaths() {
79         return mImportPaths;
80     }
81 
getSources()82     public Set<String> getSources() {
83         return mSources;
84     }
85 
getHostOnly()86     public boolean getHostOnly() {
87         return mHostOnly;
88     }
89 
90     /**
91      * Get a {@link String} represent the test name and its host setting. This allows TestInfos to
92      * be grouped by name the requirement on device.
93      */
getNameAndHostOnly()94     public String getNameAndHostOnly() {
95         return String.format("%s - %s", mName, mHostOnly);
96     }
97 
98     /** Get a {@link String} represent the test name and its options. */
getNameOption()99     public String getNameOption() {
100         return String.format("%s%s", mName, mOptions.toString());
101     }
102 
103     /** Get a {@link Set} of the keywords supported by the test. */
getKeywords()104     public Set<String> getKeywords() {
105         return new HashSet<>(mKeywords);
106     }
107 
108     /**
109      * Get a {@link Set} of the keywords supported by the test.
110      *
111      * @param ignoreKeywords A set of {@link String} of keywords to be ignored.
112      */
getKeywords(Set<String> ignoreKeywords)113     public Set<String> getKeywords(Set<String> ignoreKeywords) {
114         Set<String> keywords = new LinkedHashSet<>(mKeywords);
115         keywords.removeAll(ignoreKeywords);
116         return keywords;
117     }
118 
119     /**
120      * Merge with another test.
121      *
122      * <p>Update test options so the test has the best possible coverage of both tests.
123      *
124      * <p>TODO(b/113616538): Implement a more robust option merging mechanism.
125      *
126      * @param test {@link TestInfo} object to be merged with.
127      */
merge(TestInfo test)128     public void merge(TestInfo test) {
129         CLog.d("Merging test %s and %s.", this, test);
130         // Merge can only happen for tests for the same module.
131         checkState(
132                 mName.equals(test.getName()), "Only TestInfo for the same module can be merged.");
133         // Merge can only happen for tests for the same device requirement.
134         checkState(
135                 mHostOnly == test.getHostOnly(),
136                 "Only TestInfo for the same device requirement (running on device or host) can"
137                         + " be merged.");
138 
139         List<TestOption> mergedOptions = new ArrayList<>();
140 
141         // If any test only has exclusive options or no option, only keep the common exclusive
142         // option in the merged test. For example:
143         // this.mOptions: include-filter=value1, exclude-annotation=flaky
144         // test.mOptions: exclude-annotation=flaky, exclude-filter=value2
145         // merged options: exclude-annotation=flaky
146         // Note that:
147         // * The exclude-annotation of flaky is common between the two tests, so it's kept.
148         // * The include-filter of value1 is dropped as `test` doesn't have any include-filter,
149         //   thus it has larger test coverage and the include-filter is ignored.
150         // * The exclude-filter of value2 is dropped as it's only for `test`. To achieve maximum
151         //   test coverage for both `this` and `test`, we shall only keep the common exclusive
152         //   filters.
153         // * In the extreme case that one of the test has no option at all, the merged test will
154         //   also have no option.
155         if (test.exclusiveOptionsOnly() || this.exclusiveOptionsOnly()) {
156             Set<TestOption> commonOptions = new HashSet<TestOption>(test.getOptions());
157             commonOptions.retainAll(new HashSet<TestOption>(mOptions));
158             mOptions = new ArrayList<TestOption>(commonOptions);
159             this.addSources(test.getSources());
160             CLog.d("Options are merged, updated test: %s.", this);
161             return;
162         }
163 
164         // When neither test has no option or with only exclusive options, we try the best to
165         // merge the test options so the merged test will cover both tests.
166         // 1. Keep all non-exclusive options, except include-annotation
167         // 2. Keep common exclusive options
168         // 3. Keep common include-annotation options
169         // 4. Keep any exclude-annotation options
170         // Condition 3 and 4 are added to make sure we have the best test coverage if possible.
171         // In most cases, one add include-annotation to include only presubmit test, but some other
172         // test config that doesn't use presubmit annotation doesn't have such option. Therefore,
173         // uncommon include-annotation option has to be dropped to prevent losing test coverage.
174         // On the other hand, exclude-annotation is often used to exclude flaky tests. Therefore,
175         // it's better to keep any exclude-annotation option to prevent flaky tests from being
176         // included.
177         // For example:
178         // this.mOptions: include-filter=value1, exclude-filter=ex-value1, exclude-filter=ex-value2,
179         //                exclude-annotation=flaky, include-annotation=presubmit
180         // test.mOptions: exclude-filter=ex-value1, include-filter=value3
181         // merged options: exclude-annotation=flaky, include-filter=value1, include-filter=value3
182         // Note that:
183         // * The "exclude-filter=value3" option is kept as it's common in both tests.
184         // * The "exclude-annotation=flaky" option is kept even though it's only in one test.
185         // * The "include-annotation=presubmit" option is dropped as it only exists for `this`.
186         // * The include-filter of value1 and value3 are both kept so the merged test will cover
187         //   both tests.
188         // * The "exclude-filter=ex-value1" option is kept as it's common in both tests.
189         // * The "exclude-filter=ex-value2" option is dropped as it's only for `this`. To achieve
190         //     maximum test coverage for both `this` and `test`, we shall only keep the common
191         //     exclusive filters.
192 
193         // Options from this test:
194         Set<TestOption> nonExclusiveOptions =
195                 mOptions.stream()
196                         .filter(
197                                 option ->
198                                         !option.isExclusive()
199                                                 && !OPTION_INCLUDE_ANNOTATION.equals(
200                                                         option.getName()))
201                         .collect(Collectors.toSet());
202         Set<TestOption> includeAnnotationOptions =
203                 mOptions.stream()
204                         .filter(option -> OPTION_INCLUDE_ANNOTATION.equals(option.getName()))
205                         .collect(Collectors.toSet());
206         Set<TestOption> exclusiveOptions =
207                 mOptions.stream()
208                         .filter(
209                                 option ->
210                                         option.isExclusive()
211                                                 && !OPTION_EXCLUDE_ANNOTATION.equals(
212                                                         option.getName()))
213                         .collect(Collectors.toSet());
214         Set<TestOption> excludeAnnotationOptions =
215                 mOptions.stream()
216                         .filter(option -> OPTION_EXCLUDE_ANNOTATION.equals(option.getName()))
217                         .collect(Collectors.toSet());
218         // Options from TestInfo to be merged:
219         Set<TestOption> nonExclusiveOptionsToMerge =
220                 test.getOptions()
221                         .stream()
222                         .filter(
223                                 option ->
224                                         !option.isExclusive()
225                                                 && !OPTION_INCLUDE_ANNOTATION.equals(
226                                                         option.getName()))
227                         .collect(Collectors.toSet());
228         Set<TestOption> includeAnnotationOptionsToMerge =
229                 test.getOptions()
230                         .stream()
231                         .filter(option -> OPTION_INCLUDE_ANNOTATION.equals(option.getName()))
232                         .collect(Collectors.toSet());
233         Set<TestOption> exclusiveOptionsToMerge =
234                 test.getOptions()
235                         .stream()
236                         .filter(
237                                 option ->
238                                         option.isExclusive()
239                                                 && !OPTION_EXCLUDE_ANNOTATION.equals(
240                                                         option.getName()))
241                         .collect(Collectors.toSet());
242         Set<TestOption> excludeAnnotationOptionsToMerge =
243                 test.getOptions()
244                         .stream()
245                         .filter(option -> OPTION_EXCLUDE_ANNOTATION.equals(option.getName()))
246                         .collect(Collectors.toSet());
247 
248         // 1. Keep all non-exclusive options, except include-annotation
249         nonExclusiveOptions.addAll(nonExclusiveOptionsToMerge);
250         for (TestOption option : nonExclusiveOptions) {
251             mergedOptions.add(option);
252         }
253         // 2. Keep common exclusive options, except exclude-annotation
254         exclusiveOptions.retainAll(exclusiveOptionsToMerge);
255         for (TestOption option : exclusiveOptions) {
256             mergedOptions.add(option);
257         }
258         // 3. Keep common include-annotation options
259         includeAnnotationOptions.retainAll(includeAnnotationOptionsToMerge);
260         for (TestOption option : includeAnnotationOptions) {
261             mergedOptions.add(option);
262         }
263         // 4. Keep any exclude-annotation options
264         excludeAnnotationOptions.addAll(excludeAnnotationOptionsToMerge);
265         for (TestOption option : excludeAnnotationOptions) {
266             mergedOptions.add(option);
267         }
268         this.mOptions = mergedOptions;
269         this.addSources(test.getSources());
270         CLog.d("Options are merged, updated test: %s.", this);
271     }
272 
273     /* Check if the TestInfo only has exclusive options.
274      *
275      * @return true if the TestInfo only has exclusive options.
276      */
exclusiveOptionsOnly()277     private boolean exclusiveOptionsOnly() {
278         for (TestOption option : mOptions) {
279             if (option.isInclusive()) {
280                 return false;
281             }
282         }
283         return true;
284     }
285 
286     @Override
equals(Object o)287     public boolean equals(Object o) {
288         return this.toString().equals(o.toString());
289     }
290 
291     @Override
hashCode()292     public int hashCode() {
293         return this.toString().hashCode();
294     }
295 
296     @Override
toString()297     public String toString() {
298         StringBuilder string = new StringBuilder();
299         string.append(mName);
300         if (!mOptions.isEmpty()) {
301             String options =
302                     String.format(
303                             "Options: %s",
304                             String.join(
305                                     ",",
306                                     mOptions.stream()
307                                             .sorted()
308                                             .map(TestOption::toString)
309                                             .collect(Collectors.toList())));
310             string.append("\n\t").append(options);
311         }
312         if (!mKeywords.isEmpty()) {
313             String keywords =
314                     String.format(
315                             "Keywords: %s",
316                             String.join(
317                                     ",", mKeywords.stream().sorted().collect(Collectors.toList())));
318             string.append("\n\t").append(keywords);
319         }
320         if (!mSources.isEmpty()) {
321             String sources =
322                     String.format(
323                             "Sources: %s",
324                             String.join(
325                                     ",", mSources.stream().sorted().collect(Collectors.toList())));
326             string.append("\n\t").append(sources);
327         }
328         string.append("\n\tHost: ").append(mHostOnly);
329         return string.toString();
330     }
331 }
332