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.tradefed.testtype; 17 18 import com.android.tradefed.build.IFolderBuildInfo; 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 22 import com.android.tradefed.result.FileInputStreamSource; 23 import com.android.tradefed.result.ITestInvocationListener; 24 import com.android.tradefed.result.InputStreamSource; 25 import com.android.tradefed.result.LogDataType; 26 import com.android.tradefed.result.TestDescription; 27 import com.android.tradefed.util.CommandResult; 28 import com.android.tradefed.util.CommandStatus; 29 import com.android.tradefed.util.FileUtil; 30 import com.android.tradefed.util.HprofAllocSiteParser; 31 import com.android.tradefed.util.RunUtil; 32 import com.android.tradefed.util.StreamUtil; 33 import com.android.tradefed.util.SystemUtil.EnvVariable; 34 import com.android.tradefed.util.proto.TfMetricProtoUtil; 35 import com.android.tradefed.util.TarUtil; 36 37 import com.google.common.annotations.VisibleForTesting; 38 39 import java.io.File; 40 import java.io.IOException; 41 import java.io.InputStream; 42 import java.util.ArrayList; 43 import java.util.Arrays; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.regex.Pattern; 48 49 /** 50 * A {@link IRemoteTest} for running unit or functional tests against a separate TF installation. 51 * <p/> 52 * Launches an external java process to run the tests. Used for running the TF unit or 53 * functional tests continuously. 54 */ 55 public class TfTestLauncher extends SubprocessTfLauncher { 56 57 private static final long COVERAGE_REPORT_TIMEOUT_MS = 2 * 60 * 1000; 58 59 @Option(name = "jacoco-code-coverage", description = "Enable jacoco code coverage on the java " 60 + "sub process. Run will be slightly slower because of the overhead.") 61 private boolean mEnableCoverage = false; 62 63 @Option( 64 name = "hprof-heap-memory", 65 description = 66 "Enable hprof agent while running the java" 67 + "sub process. Run will be slightly slower because of the overhead." 68 ) 69 private boolean mEnableHprof = false; 70 71 @Option(name = "ant-config-res", description = "The name of the ant resource configuration to " 72 + "transform the results in readable format.") 73 private String mAntConfigResource = "/jacoco/ant-tf-coverage.xml"; 74 75 @Option(name = "sub-branch", description = "The branch to be provided to the sub invocation, " 76 + "if null, the branch in build info will be used.") 77 private String mSubBranch = null; 78 79 @Option(name = "sub-build-flavor", description = "The build flavor to be provided to the " 80 + "sub invocation, if null, the build flavor in build info will be used.") 81 private String mSubBuildFlavor = null; 82 83 @Option(name = "sub-build-id", description = "The build id that the sub invocation will try " 84 + "to use in case where it needs its own device.") 85 private String mSubBuildId = null; 86 87 @Option(name = "use-virtual-device", description = 88 "Flag if the subprocess is going to need to instantiate a virtual device to run.") 89 private boolean mUseVirtualDevice = false; 90 91 @Option(name = "sub-apk-path", description = "The name of all the Apks that needs to be " 92 + "installed by the subprocess invocation. Apk need to be inside the downloaded zip. " 93 + "Can be repeated.") 94 private List<String> mSubApkPath = new ArrayList<String>(); 95 96 // The regex pattern of temp files to be found in the temporary dir of the subprocess. 97 // Any file not matching the patterns, or multiple files in the temporary dir match the same 98 // pattern, is considered as test failure. 99 private static final String[] EXPECTED_TMP_FILE_PATTERNS = { 100 "inv_.*", "tradefed_global_log_.*", "lc_cache", "stage-android-build-api", 101 }; 102 103 // A destination file where the report will be put. 104 private File mDestCoverageFile = null; 105 // A destination file where the hprof report will be put. 106 private File mHprofFile = null; 107 // A {@link File} pointing to the jacoco args jar file extracted from the resources 108 private File mAgent = null; 109 110 /** {@inheritDoc} */ 111 @Override 112 protected void addJavaArguments(List<String> args) { 113 super.addJavaArguments(args); 114 try { 115 if (mEnableCoverage) { 116 mDestCoverageFile = FileUtil.createTempFile("coverage", ".exec"); 117 mAgent = extractJacocoAgent(); 118 addCoverageArgs(mAgent, args, mDestCoverageFile); 119 } 120 if (mEnableHprof) { 121 mHprofFile = FileUtil.createTempFile("java.hprof", ".txt"); 122 // verbose=n to avoid dump in stderr 123 // cutoff the min value we look at. 124 String hprofAgent = 125 String.format( 126 "-agentlib:hprof=heap=sites,cutoff=0.01,depth=16,verbose=n,file=%s", 127 mHprofFile.getAbsolutePath()); 128 args.add(hprofAgent); 129 } 130 } catch (IOException e) { 131 throw new RuntimeException(e); 132 } 133 } 134 135 /** {@inheritDoc} */ 136 @Override 137 protected void preRun() { 138 super.preRun(); 139 140 if (!mUseVirtualDevice) { 141 mCmdArgs.add("-n"); 142 } else { 143 // if it needs a device we also enable more logs 144 mCmdArgs.add("--log-level"); 145 mCmdArgs.add("VERBOSE"); 146 mCmdArgs.add("--log-level-display"); 147 mCmdArgs.add("VERBOSE"); 148 } 149 mCmdArgs.add("--test-tag"); 150 mCmdArgs.add(mBuildInfo.getTestTag()); 151 mCmdArgs.add("--build-id"); 152 if (mSubBuildId != null) { 153 mCmdArgs.add(mSubBuildId); 154 } else { 155 mCmdArgs.add(mBuildInfo.getBuildId()); 156 } 157 mCmdArgs.add("--branch"); 158 if (mSubBranch != null) { 159 mCmdArgs.add(mSubBranch); 160 } else if (mBuildInfo.getBuildBranch() != null) { 161 mCmdArgs.add(mBuildInfo.getBuildBranch()); 162 } else { 163 throw new RuntimeException("Branch option is required for the sub invocation."); 164 } 165 mCmdArgs.add("--build-flavor"); 166 if (mSubBuildFlavor != null) { 167 mCmdArgs.add(mSubBuildFlavor); 168 } else if (mBuildInfo.getBuildFlavor() != null) { 169 mCmdArgs.add(mBuildInfo.getBuildFlavor()); 170 } else { 171 throw new RuntimeException("Build flavor option is required for the sub invocation."); 172 } 173 174 for (String apk : mSubApkPath) { 175 mCmdArgs.add("--apk-path"); 176 String apkPath = 177 String.format( 178 "%s%s%s", 179 ((IFolderBuildInfo) mBuildInfo).getRootDir().getAbsolutePath(), 180 File.separator, 181 apk); 182 mCmdArgs.add(apkPath); 183 } 184 // Unset potential build environment to ensure they do not affect the unit tests 185 getRunUtil().unsetEnvVariable(EnvVariable.ANDROID_HOST_OUT_TESTCASES.name()); 186 getRunUtil().unsetEnvVariable(EnvVariable.ANDROID_TARGET_OUT_TESTCASES.name()); 187 } 188 189 /** {@inheritDoc} */ 190 @Override 191 protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) { 192 super.postRun(listener, exception, elapsedTime); 193 reportMetrics(elapsedTime, listener); 194 FileUtil.deleteFile(mAgent); 195 196 // Evaluate coverage from the subprocess 197 if (mEnableCoverage) { 198 InputStreamSource coverage = null; 199 File xmlResult = null; 200 try { 201 xmlResult = processExecData(mDestCoverageFile, mRootDir); 202 coverage = new FileInputStreamSource(xmlResult); 203 listener.testLog("coverage_xml", LogDataType.JACOCO_XML, coverage); 204 } catch (IOException e) { 205 if (exception) { 206 // If exception was thrown above, we only log this one since it's most 207 // likely related to it. 208 CLog.e(e); 209 } else { 210 throw new RuntimeException(e); 211 } 212 } finally { 213 FileUtil.deleteFile(mDestCoverageFile); 214 StreamUtil.cancel(coverage); 215 FileUtil.deleteFile(xmlResult); 216 } 217 } 218 if (mEnableHprof) { 219 logHprofResults(mHprofFile, listener); 220 } 221 222 if (mTmpDir != null) { 223 testTmpDirClean(mTmpDir, listener); 224 } 225 cleanTmpFile(); 226 } 227 228 @VisibleForTesting 229 void cleanTmpFile() { 230 FileUtil.deleteFile(mHprofFile); 231 FileUtil.deleteFile(mDestCoverageFile); 232 FileUtil.deleteFile(mAgent); 233 } 234 235 /** 236 * Helper to add arguments required for code coverage collection. 237 * 238 * @param jacocoAgent the jacoco args file to run the coverage. 239 * @param args list of arguments that will be run in the subprocess. 240 * @param destfile destination file where the report will be put. 241 */ 242 private void addCoverageArgs(File jacocoAgent, List<String> args, File destfile) { 243 String javaagent = String.format("-javaagent:%s=destfile=%s," 244 + "includes=com.android.tradefed*:com.google.android.tradefed*", 245 jacocoAgent.getAbsolutePath(), 246 destfile.getAbsolutePath()); 247 args.add(javaagent); 248 } 249 250 /** 251 * Returns a {@link File} pointing to the jacoco args jar file extracted from the resources. 252 */ 253 private File extractJacocoAgent() throws IOException { 254 String jacocoAgentRes = "/jacoco/jacocoagent.jar"; 255 InputStream jacocoAgentStream = getClass().getResourceAsStream(jacocoAgentRes); 256 if (jacocoAgentStream == null) { 257 throw new IOException("Could not find " + jacocoAgentRes); 258 } 259 File jacocoAgent = FileUtil.createTempFile("jacocoagent", ".jar"); 260 FileUtil.writeToFile(jacocoAgentStream, jacocoAgent); 261 return jacocoAgent; 262 } 263 264 /** 265 * Helper to process the execution data into user readable format (xml) that can easily be 266 * parsed. 267 * 268 * @param executionData output files of the java args jacoco. 269 * @param rootDir base directory of downloaded TF 270 * @return a {@link File} pointing to the human readable xml result file. 271 */ 272 private File processExecData(File executionData, String rootDir) throws IOException { 273 File xmlReport = FileUtil.createTempFile("coverage_xml", ".xml"); 274 InputStream template = getClass().getResourceAsStream(mAntConfigResource); 275 if (template == null) { 276 throw new IOException("Could not find " + mAntConfigResource); 277 } 278 String jacocoAntRes = "/jacoco/jacocoant.jar"; 279 InputStream jacocoAntStream = getClass().getResourceAsStream(jacocoAntRes); 280 if (jacocoAntStream == null) { 281 throw new IOException("Could not find " + jacocoAntRes); 282 } 283 File antConfig = FileUtil.createTempFile("ant-merge_", ".xml"); 284 File jacocoAnt = FileUtil.createTempFile("jacocoant", ".jar"); 285 try { 286 FileUtil.writeToFile(template, antConfig); 287 FileUtil.writeToFile(jacocoAntStream, jacocoAnt); 288 String[] cmd = {"ant", "-f", antConfig.getPath(), 289 "-Djacocoant.path=" + jacocoAnt.getAbsolutePath(), 290 "-Dexecution.files=" + executionData.getAbsolutePath(), 291 "-Droot.dir=" + rootDir, 292 "-Ddest.file=" + xmlReport.getAbsolutePath()}; 293 CommandResult result = RunUtil.getDefault().runTimedCmd(COVERAGE_REPORT_TIMEOUT_MS, 294 cmd); 295 CLog.d(result.getStdout()); 296 if (!CommandStatus.SUCCESS.equals(result.getStatus())) { 297 throw new IOException(result.getStderr()); 298 } 299 return xmlReport; 300 } finally { 301 FileUtil.deleteFile(antConfig); 302 FileUtil.deleteFile(jacocoAnt); 303 } 304 } 305 306 /** 307 * Report an elapsed-time metric to keep track of it. 308 * 309 * @param elapsedTime time it took the subprocess to run. 310 * @param listener the {@link ITestInvocationListener} where to report the metric. 311 */ 312 private void reportMetrics(long elapsedTime, ITestInvocationListener listener) { 313 if (elapsedTime == -1l) { 314 return; 315 } 316 listener.testRunStarted("elapsed-time", 1); 317 TestDescription tid = new TestDescription("elapsed-time", "run-elapsed-time"); 318 listener.testStarted(tid); 319 HashMap<String, Metric> runMetrics = new HashMap<>(); 320 runMetrics.put( 321 "elapsed-time", TfMetricProtoUtil.stringToMetric(Long.toString(elapsedTime))); 322 listener.testEnded(tid, runMetrics); 323 listener.testRunEnded(elapsedTime, runMetrics); 324 } 325 326 /** 327 * Extra test to ensure no files are created by the unit tests in the subprocess and not 328 * cleaned. 329 * 330 * @param tmpDir the temporary dir of the subprocess. 331 * @param listener the {@link ITestInvocationListener} where to report the test. 332 */ 333 @VisibleForTesting 334 protected void testTmpDirClean(File tmpDir, ITestInvocationListener listener) { 335 listener.testRunStarted("temporaryFiles", 1); 336 TestDescription tid = new TestDescription("temporary-files", "testIfClean"); 337 listener.testStarted(tid); 338 String[] listFiles = tmpDir.list(); 339 List<String> unmatchedFiles = new ArrayList<String>(); 340 List<String> patterns = new ArrayList<String>(Arrays.asList(EXPECTED_TMP_FILE_PATTERNS)); 341 for (String file : Arrays.asList(listFiles)) { 342 Boolean matchFound = false; 343 for (String pattern : patterns) { 344 if (Pattern.matches(pattern, file)) { 345 patterns.remove(pattern); 346 matchFound = true; 347 break; 348 } 349 } 350 if (!matchFound) { 351 unmatchedFiles.add(file); 352 } 353 } 354 if (unmatchedFiles.size() > 0) { 355 String trace = String.format("Found '%d' unexpected temporary files: %s.\nOnly " 356 + "expected files are: %s. And each should appears only once.", 357 unmatchedFiles.size(), unmatchedFiles, 358 Arrays.asList(EXPECTED_TMP_FILE_PATTERNS)); 359 listener.testFailed(tid, trace); 360 } 361 listener.testEnded(tid, new HashMap<String, Metric>()); 362 listener.testRunEnded(0, new HashMap<String, Metric>()); 363 } 364 365 /** 366 * Helper to log and report as metric the hprof data. 367 * 368 * @param hprofFile file containing the Hprof report 369 * @param listener the {@link ITestInvocationListener} where to report the test. 370 */ 371 private void logHprofResults(File hprofFile, ITestInvocationListener listener) { 372 if (hprofFile == null) { 373 CLog.w("Hprof file was null. Skipping parsing."); 374 return; 375 } 376 if (!hprofFile.exists()) { 377 CLog.w("Hprof file %s was not found. Skipping parsing.", hprofFile.getAbsolutePath()); 378 return; 379 } 380 InputStreamSource memory = null; 381 File tmpGzip = null; 382 try { 383 tmpGzip = TarUtil.gzip(hprofFile); 384 memory = new FileInputStreamSource(tmpGzip); 385 listener.testLog("hprof", LogDataType.GZIP, memory); 386 } catch (IOException e) { 387 CLog.e(e); 388 return; 389 } finally { 390 StreamUtil.cancel(memory); 391 FileUtil.deleteFile(tmpGzip); 392 } 393 HprofAllocSiteParser parser = new HprofAllocSiteParser(); 394 try { 395 Map<String, String> results = parser.parse(hprofFile); 396 if (results.isEmpty()) { 397 CLog.d("No allocation site found from hprof file"); 398 return; 399 } 400 listener.testRunStarted("hprofAllocSites", 1); 401 TestDescription tid = new TestDescription("hprof", "allocationSites"); 402 listener.testStarted(tid); 403 listener.testEnded(tid, TfMetricProtoUtil.upgradeConvert(results)); 404 listener.testRunEnded(0, new HashMap<String, Metric>()); 405 } catch (IOException e) { 406 throw new RuntimeException(e); 407 } 408 } 409 } 410