1 /* 2 * Copyright (C) 2016 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.compatibility.common.tradefed.result; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.testtype.ISubPlan; 20 import com.android.compatibility.common.tradefed.testtype.SubPlan; 21 import com.android.compatibility.common.tradefed.testtype.suite.CompatibilityTestSuite; 22 import com.android.compatibility.common.tradefed.util.OptionHelper; 23 import com.android.compatibility.common.util.ICaseResult; 24 import com.android.compatibility.common.util.IInvocationResult; 25 import com.android.compatibility.common.util.IModuleResult; 26 import com.android.compatibility.common.util.ITestResult; 27 import com.android.compatibility.common.util.ResultHandler; 28 import com.android.compatibility.common.util.TestFilter; 29 import com.android.compatibility.common.util.TestStatus; 30 import com.android.ddmlib.Log.LogLevel; 31 import com.android.tradefed.config.ArgsOptionParser; 32 import com.android.tradefed.config.ConfigurationException; 33 import com.android.tradefed.config.Option; 34 import com.android.tradefed.config.Option.Importance; 35 import com.android.tradefed.log.LogUtil.CLog; 36 import com.android.tradefed.util.StreamUtil; 37 import com.android.tradefed.util.xml.AbstractXmlParser.ParseException; 38 39 import com.google.common.annotations.VisibleForTesting; 40 41 import java.io.BufferedOutputStream; 42 import java.io.File; 43 import java.io.FileInputStream; 44 import java.io.FileNotFoundException; 45 import java.io.FileOutputStream; 46 import java.io.IOException; 47 import java.io.InputStream; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.Collections; 51 import java.util.HashMap; 52 import java.util.HashSet; 53 import java.util.Map; 54 import java.util.Set; 55 56 /** 57 * Class for creating subplans from compatibility result XML. 58 */ 59 public class SubPlanHelper { 60 61 private static final String XML_EXT = ".xml"; 62 63 // string signalling the beginning of the parameter in a test name 64 private static final String PARAM_START = "["; 65 66 // result types 67 public static final String PASSED = "passed"; 68 public static final String FAILED = "failed"; 69 public static final String NOT_EXECUTED = "not_executed"; 70 // static mapping of result types to TestStatuses 71 private static final Map<String, TestStatus> STATUS_MAP; 72 static { 73 Map<String, TestStatus> statusMap = new HashMap<String, TestStatus>(); statusMap.put(PASSED, TestStatus.PASS)74 statusMap.put(PASSED, TestStatus.PASS); statusMap.put(FAILED, TestStatus.FAIL)75 statusMap.put(FAILED, TestStatus.FAIL); 76 STATUS_MAP = Collections.unmodifiableMap(statusMap); 77 } 78 79 @Option (name = "name", shortName = 'n', description = "the name of the subplan to create", 80 importance=Importance.IF_UNSET) 81 private String mSubPlanName = null; 82 83 @Option (name = "session", description = "the session id to derive from", 84 importance=Importance.IF_UNSET) 85 private Integer mSessionId = null; 86 87 @Option (name = "result-type", 88 description = "the result type to include. One of passed, failed, not_executed." 89 + " Option may be repeated", 90 importance=Importance.IF_UNSET) 91 private Set<String> mResultTypes = new HashSet<String>(); 92 93 @Option(name = CompatibilityTestSuite.INCLUDE_FILTER_OPTION, 94 description = "the include module filters to apply.", 95 importance = Importance.NEVER) 96 private Set<String> mIncludeFilters = new HashSet<String>(); 97 98 @Option(name = CompatibilityTestSuite.EXCLUDE_FILTER_OPTION, 99 description = "the exclude module filters to apply.", 100 importance = Importance.NEVER) 101 private Set<String> mExcludeFilters = new HashSet<String>(); 102 103 @Option(name = CompatibilityTestSuite.MODULE_OPTION, shortName = 'm', 104 description = "the test module to run.", 105 importance = Importance.NEVER) 106 private String mModuleName = null; 107 108 @Option(name = CompatibilityTestSuite.TEST_OPTION, shortName = 't', 109 description = "the test to run.", 110 importance = Importance.NEVER) 111 private String mTestName = null; 112 113 @Option(name = CompatibilityTestSuite.ABI_OPTION, shortName = 'a', 114 description = "the abi to test.", 115 importance = Importance.NEVER) 116 private String mAbiName = null; 117 118 @Option(name = CompatibilityTestSuite.SUBPLAN_OPTION, 119 description = "the subplan used in the previous session", 120 importance = Importance.NEVER) 121 private String mLastSubPlan; 122 123 File mSubPlanFile = null; 124 IInvocationResult mResult = null; 125 126 /** 127 * Create an empty {@link SubPlanHelper}. 128 * <p/> 129 * All {@link Option} fields must be populated via 130 * {@link com.android.tradefed.config.ArgsOptionParser} 131 */ SubPlanHelper()132 public SubPlanHelper() {} 133 134 /** 135 * Create a {@link SubPlanHelper} using the specified option values. 136 */ SubPlanHelper(String name, int session, Collection<String> resultTypes)137 public SubPlanHelper(String name, int session, Collection<String> resultTypes) { 138 mSubPlanName = name; 139 mSessionId = session; 140 mResultTypes.addAll(resultTypes); 141 } 142 getSubPlanByName(CompatibilityBuildHelper buildHelper, String name)143 public static ISubPlan getSubPlanByName(CompatibilityBuildHelper buildHelper, String name) { 144 if (!name.endsWith(XML_EXT)) { 145 name = name + XML_EXT; // only append XML extension to name if not already there 146 } 147 InputStream subPlanInputStream = null; 148 try { 149 File subPlanFile = new File(buildHelper.getSubPlansDir(), name); 150 if (!subPlanFile.exists()) { 151 throw new IllegalArgumentException( 152 String.format("Could not retrieve subplan \"%s\"", name)); 153 } 154 subPlanInputStream = new FileInputStream(subPlanFile); 155 ISubPlan subPlan = new SubPlan(); 156 subPlan.parse(subPlanInputStream); 157 return subPlan; 158 } catch (FileNotFoundException | ParseException e) { 159 throw new RuntimeException( 160 String.format("Unable to find or parse subplan %s", name), e); 161 } finally { 162 StreamUtil.close(subPlanInputStream); 163 } 164 } 165 166 /** 167 * Set the result from which to derive the subplan. 168 * @param result 169 */ setResult(IInvocationResult result)170 public void setResult(IInvocationResult result) { 171 mResult = result; 172 } 173 174 /** 175 * Add a result type from which to derive the subplan. PASSED, FAILED, or NOT_EXECUTED 176 * @param resultType 177 */ addResultType(String resultType)178 public void addResultType(String resultType) { 179 mResultTypes.add(resultType); 180 } 181 182 /** 183 * Create and serialize a subplan derived from a result. 184 * <p/> 185 * {@link Option} values must all be set before this is called. 186 * @return serialized subplan file. 187 * @throws ConfigurationException 188 */ createAndSerializeSubPlan(CompatibilityBuildHelper buildHelper)189 public File createAndSerializeSubPlan(CompatibilityBuildHelper buildHelper) 190 throws ConfigurationException { 191 ISubPlan subPlan = createSubPlan(buildHelper); 192 if (subPlan != null) { 193 try { 194 subPlan.serialize(new BufferedOutputStream(new FileOutputStream(mSubPlanFile))); 195 CLog.logAndDisplay(LogLevel.INFO, "Created subplan \"%s\" at %s", 196 mSubPlanName, mSubPlanFile.getAbsolutePath()); 197 return mSubPlanFile; 198 } catch (IOException e) { 199 CLog.e("Failed to create plan file %s", mSubPlanFile.getAbsolutePath()); 200 CLog.e(e); 201 } 202 } 203 return null; 204 } 205 206 /** 207 * Create a subplan derived from a result. 208 * <p/> 209 * {@link Option} values must be set before this is called. 210 * @param buildHelper 211 * @return subplan 212 * @throws ConfigurationException 213 */ createSubPlan(CompatibilityBuildHelper buildHelper)214 public ISubPlan createSubPlan(CompatibilityBuildHelper buildHelper) 215 throws ConfigurationException { 216 setupFields(buildHelper); 217 ISubPlan subPlan = new SubPlan(); 218 219 // add filters from previous session to track which tests must run 220 subPlan.addAllIncludeFilters(mIncludeFilters); 221 subPlan.addAllExcludeFilters(mExcludeFilters); 222 if (mLastSubPlan != null) { 223 ISubPlan lastSubPlan = SubPlanHelper.getSubPlanByName(buildHelper, mLastSubPlan); 224 subPlan.addAllIncludeFilters(lastSubPlan.getIncludeFilters()); 225 subPlan.addAllExcludeFilters(lastSubPlan.getExcludeFilters()); 226 } 227 if (mModuleName != null) { 228 addIncludeToSubPlan(subPlan, new TestFilter(mAbiName, mModuleName, mTestName)); 229 } 230 Set<TestStatus> statusesToRun = getStatusesToRun(); 231 for (IModuleResult module : mResult.getModules()) { 232 if (shouldRunModule(module)) { 233 TestFilter moduleInclude = 234 new TestFilter(module.getAbi(), module.getName(), null /*test*/); 235 if (shouldRunEntireModule(module)) { 236 // include entire module 237 addIncludeToSubPlan(subPlan, moduleInclude); 238 } else if (mResultTypes.contains(NOT_EXECUTED) && !module.isDone()) { 239 // add module include and test excludes 240 addIncludeToSubPlan(subPlan, moduleInclude); 241 for (ICaseResult caseResult : module.getResults()) { 242 for (ITestResult testResult : caseResult.getResults()) { 243 if (!statusesToRun.contains(testResult.getResultStatus())) { 244 TestFilter testExclude = new TestFilter(module.getAbi(), 245 module.getName(), testResult.getFullName()); 246 addExcludeToSubPlan(subPlan, testExclude); 247 } 248 } 249 } 250 } else { 251 // Not-executed tests should not be rerun and/or this module is completed 252 // In any such case, it suffices to add includes for each test to rerun 253 for (ICaseResult caseResult : module.getResults()) { 254 for (ITestResult testResult : caseResult.getResults()) { 255 if (statusesToRun.contains(testResult.getResultStatus())) { 256 TestFilter testInclude = new TestFilter(module.getAbi(), 257 module.getName(), testResult.getFullName()); 258 addIncludeToSubPlan(subPlan, testInclude); 259 } 260 } 261 } 262 } 263 } else { 264 // module should not run, exclude entire module 265 TestFilter moduleExclude = 266 new TestFilter(module.getAbi(), module.getName(), null /*test*/); 267 addExcludeToSubPlan(subPlan, moduleExclude); 268 } 269 } 270 return subPlan; 271 } 272 273 /** 274 * Add the include test filter to the subplan. For filters that specify the parameters of a 275 * test, strip the parameter suffix and add the include, which will run the test with all 276 * parameters. If JUnit test runners are extended to handle filtering by parameter, this 277 * special case may be removed. 278 */ 279 @VisibleForTesting addIncludeToSubPlan(ISubPlan subPlan, TestFilter include)280 static void addIncludeToSubPlan(ISubPlan subPlan, TestFilter include) { 281 String test = include.getTest(); 282 String str = include.toString(); 283 if (test == null || !test.contains(PARAM_START)) { 284 subPlan.addIncludeFilter(str); 285 } else if (test.contains(PARAM_START)) { 286 // filter applies to parameterized test, include test without parameter. 287 subPlan.addIncludeFilter(str.substring(0, str.lastIndexOf(PARAM_START))); 288 } 289 } 290 291 /** 292 * Add the exclude test filter to the subplan. For filters that specify the parameters of a 293 * test, do not add the exclude filter. This will prompt the test to run again with all 294 * parameters. If JUnit test runners are extended to handle filtering by parameter, this 295 * special case may be removed. 296 */ 297 @VisibleForTesting addExcludeToSubPlan(ISubPlan subPlan, TestFilter exclude)298 static void addExcludeToSubPlan(ISubPlan subPlan, TestFilter exclude) { 299 String test = exclude.getTest(); 300 String str = exclude.toString(); 301 if (test == null || !test.contains(PARAM_START)) { 302 subPlan.addExcludeFilter(str); 303 } 304 // don't add exclude for parameterized test, as runners do not support this. 305 } 306 307 /** 308 * Whether any tests within the given {@link IModuleResult} should be run, based on 309 * the content of mResultTypes. 310 * @param module 311 * @return true if at least one test in the module should run 312 */ shouldRunModule(IModuleResult module)313 private boolean shouldRunModule(IModuleResult module) { 314 if (mResultTypes.contains(NOT_EXECUTED) && !module.isDone()) { 315 // module has not executed tests that the subplan should run 316 return true; 317 } 318 for (TestStatus status : getStatusesToRun()) { 319 if (module.countResults(status) > 0) { 320 return true; 321 } 322 } 323 return false; 324 } 325 326 /** 327 * Whether all tests within the given {@link IModuleResult} should be run, based on 328 * the content of mResultTypes. 329 * @param module 330 * @return true if every test in the module should run 331 */ shouldRunEntireModule(IModuleResult module)332 private boolean shouldRunEntireModule(IModuleResult module) { 333 if (!mResultTypes.contains(NOT_EXECUTED) && !module.isDone()) { 334 // module has not executed tests that the subplan should not run 335 return false; 336 } 337 Set<TestStatus> statusesToRun = getStatusesToRun(); 338 for (TestStatus status : TestStatus.values()) { 339 if (!statusesToRun.contains(status)) { 340 // status is a TestStatus we don't want to run 341 if (module.countResults(status) > 0) { 342 return false; 343 } 344 } 345 } 346 return true; 347 } 348 349 /** 350 * Retrieves a {@link Set} of {@link TestStatus}es to run, based on the content of 351 * mResultTypes. Does not account for result type NOT_EXECUTED, since no such TestStatus 352 * exists. 353 * @return set of TestStatuses to run 354 */ getStatusesToRun()355 private Set<TestStatus> getStatusesToRun() { 356 Set<TestStatus> statusesToRun = new HashSet<TestStatus>(); 357 for (String resultType : mResultTypes) { 358 // no test status exists for not-executed tests 359 if (!NOT_EXECUTED.equals(resultType)) { 360 statusesToRun.add(STATUS_MAP.get(resultType)); 361 } 362 } 363 return statusesToRun; 364 } 365 366 /** 367 * Ensure that all {@Option}s and fields are populated with valid values. 368 * @param buildHelper 369 * @throws ConfigurationException if any option has an invalid value 370 */ setupFields(CompatibilityBuildHelper buildHelper)371 private void setupFields(CompatibilityBuildHelper buildHelper) throws ConfigurationException { 372 if (mResult == null) { 373 if (mSessionId == null) { 374 throw new ConfigurationException("Missing --session argument"); 375 } 376 try { 377 mResult = ResultHandler.findResult(buildHelper.getResultsDir(), mSessionId); 378 } catch (FileNotFoundException e) { 379 throw new RuntimeException(e); 380 } 381 if (mResult == null) { 382 throw new IllegalArgumentException(String.format( 383 "Could not find session with id %d", mSessionId)); 384 } 385 } 386 387 String retryCommandLineArgs = mResult.getCommandLineArgs(); 388 if (retryCommandLineArgs != null) { 389 try { 390 // parse the command-line string from the result file and set options 391 ArgsOptionParser parser = new ArgsOptionParser(this); 392 parser.parse(OptionHelper.getValidCliArgs(retryCommandLineArgs, this)); 393 } catch (ConfigurationException e) { 394 throw new RuntimeException(e); 395 } 396 } 397 398 if (mResultTypes.isEmpty()) { 399 // add all valid values, include all tests of all statuses 400 mResultTypes.addAll( 401 new HashSet<String>(Arrays.asList(PASSED, FAILED, NOT_EXECUTED))); 402 } 403 // validate all test status values 404 for (String type : mResultTypes) { 405 if (!type.equals(PASSED) && !type.equals(FAILED) && !type.equals(NOT_EXECUTED)) { 406 throw new ConfigurationException(String.format("result type %s invalid", type)); 407 } 408 } 409 410 if (mSubPlanName == null) { 411 mSubPlanName = createPlanName(); 412 } 413 try { 414 mSubPlanFile = new File(buildHelper.getSubPlansDir(), mSubPlanName + XML_EXT); 415 if (mSubPlanFile.exists()) { 416 throw new ConfigurationException(String.format("Subplan %s already exists", 417 mSubPlanName)); 418 } 419 } catch (IOException e) { 420 throw new RuntimeException("Could not find subplans directory"); 421 } 422 } 423 424 /** 425 * Helper to create a plan name if none is explicitly set 426 */ createPlanName()427 private String createPlanName() { 428 StringBuilder sb = new StringBuilder(); 429 sb.append(String.join("_", mResultTypes)); 430 sb.append("_"); 431 if (mSessionId != null) { 432 sb.append(Integer.toString(mSessionId)); 433 sb.append("_"); 434 } 435 // use unique start time for name 436 sb.append(CompatibilityBuildHelper.getDirSuffix(mResult.getStartTime())); 437 return sb.toString(); 438 } 439 } 440