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