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