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