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