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.FailureDescription; 23 import com.android.tradefed.result.FileInputStreamSource; 24 import com.android.tradefed.result.ITestInvocationListener; 25 import com.android.tradefed.result.InputStreamSource; 26 import com.android.tradefed.result.LogDataType; 27 import com.android.tradefed.result.TestDescription; 28 import com.android.tradefed.util.FileUtil; 29 import com.android.tradefed.util.HprofAllocSiteParser; 30 import com.android.tradefed.util.StreamUtil; 31 import com.android.tradefed.util.SystemUtil.EnvVariable; 32 import com.android.tradefed.util.TarUtil; 33 import com.android.tradefed.util.proto.TfMetricProtoUtil; 34 35 import com.google.common.annotations.VisibleForTesting; 36 37 import java.io.File; 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.HashMap; 42 import java.util.LinkedHashSet; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 import java.util.regex.Pattern; 47 48 /** 49 * A {@link IRemoteTest} for running unit or functional tests against a separate TF installation. 50 * <p/> 51 * Launches an external java process to run the tests. Used for running the TF unit or 52 * functional tests continuously. 53 */ 54 public class TfTestLauncher extends SubprocessTfLauncher { 55 56 @Option(name = "jacoco-code-coverage", description = "Enable jacoco code coverage on the java " 57 + "sub process. Run will be slightly slower because of the overhead.") 58 private boolean mEnableCoverage = false; 59 60 @Option(name = "include-coverage", description = "Patterns to include in the code coverage.") 61 private Set<String> mIncludeCoverage = new LinkedHashSet<>(); 62 63 @Option(name = "exclude-coverage", description = "Patterns to exclude in the code coverage.") 64 private Set<String> mExcludeCoverage = new LinkedHashSet<>(); 65 66 @Option( 67 name = "hprof-heap-memory", 68 description = 69 "Enable hprof agent while running the java" 70 + "sub process. Run will be slightly slower because of the overhead." 71 ) 72 private boolean mEnableHprof = false; 73 74 @Option(name = "ant-config-res", description = "The name of the ant resource configuration to " 75 + "transform the results in readable format.") 76 private String mAntConfigResource = "/jacoco/ant-tf-coverage.xml"; 77 78 @Option(name = "sub-branch", description = "The branch to be provided to the sub invocation, " 79 + "if null, the branch in build info will be used.") 80 private String mSubBranch = null; 81 82 @Option(name = "sub-build-flavor", description = "The build flavor to be provided to the " 83 + "sub invocation, if null, the build flavor in build info will be used.") 84 private String mSubBuildFlavor = null; 85 86 @Option(name = "sub-build-id", description = "The build id that the sub invocation will try " 87 + "to use in case where it needs its own device.") 88 private String mSubBuildId = null; 89 90 @Option(name = "use-virtual-device", description = 91 "Flag if the subprocess is going to need to instantiate a virtual device to run.") 92 private boolean mUseVirtualDevice = false; 93 94 @Option(name = "sub-apk-path", description = "The name of all the Apks that needs to be " 95 + "installed by the subprocess invocation. Apk need to be inside the downloaded zip. " 96 + "Can be repeated.") 97 private List<String> mSubApkPath = new ArrayList<String>(); 98 99 @Option(name = "skip-temp-dir-check", description = "Whether or not to skip temp dir check.") 100 private boolean mSkipTmpDirCheck = false; 101 102 // The regex pattern of temp files to be found in the temporary dir of the subprocess. 103 // Any file not matching the patterns, or multiple files in the temporary dir match the same 104 // pattern, is considered as test failure. 105 private static final String[] EXPECTED_TMP_FILE_PATTERNS = { 106 "inv_.*", "tradefed_global_log_.*", "lc_cache", "stage-android-build-api", 107 }; 108 // A destination file where the hprof report will be put. 109 private File mHprofFile = null; 110 111 /** {@inheritDoc} */ 112 @Override addJavaArguments(List<String> args)113 protected void addJavaArguments(List<String> args) { 114 super.addJavaArguments(args); 115 try { 116 if (mEnableHprof) { 117 mHprofFile = FileUtil.createTempFile("java.hprof", ".txt"); 118 // verbose=n to avoid dump in stderr 119 // cutoff the min value we look at. 120 String hprofAgent = 121 String.format( 122 "-agentlib:hprof=heap=sites,cutoff=0.01,depth=16,verbose=n,file=%s", 123 mHprofFile.getAbsolutePath()); 124 args.add(hprofAgent); 125 } 126 } catch (IOException e) { 127 throw new RuntimeException(e); 128 } 129 } 130 131 /** {@inheritDoc} */ 132 @Override preRun()133 protected void preRun() { 134 super.preRun(); 135 136 if (!mUseVirtualDevice) { 137 mCmdArgs.add("-n"); 138 } else { 139 // if it needs a device we also enable more logs 140 mCmdArgs.add("--log-level"); 141 mCmdArgs.add("VERBOSE"); 142 mCmdArgs.add("--log-level-display"); 143 mCmdArgs.add("VERBOSE"); 144 } 145 mCmdArgs.add("--test-tag"); 146 mCmdArgs.add(mBuildInfo.getTestTag()); 147 mCmdArgs.add("--build-id"); 148 if (mSubBuildId != null) { 149 mCmdArgs.add(mSubBuildId); 150 } else { 151 mCmdArgs.add(mBuildInfo.getBuildId()); 152 } 153 mCmdArgs.add("--branch"); 154 if (mSubBranch != null) { 155 mCmdArgs.add(mSubBranch); 156 } else if (mBuildInfo.getBuildBranch() != null) { 157 mCmdArgs.add(mBuildInfo.getBuildBranch()); 158 } else { 159 throw new RuntimeException("Branch option is required for the sub invocation."); 160 } 161 mCmdArgs.add("--build-flavor"); 162 if (mSubBuildFlavor != null) { 163 mCmdArgs.add(mSubBuildFlavor); 164 } else if (mBuildInfo.getBuildFlavor() != null) { 165 mCmdArgs.add(mBuildInfo.getBuildFlavor()); 166 } else { 167 throw new RuntimeException("Build flavor option is required for the sub invocation."); 168 } 169 170 for (String apk : mSubApkPath) { 171 mCmdArgs.add("--apk-path"); 172 String apkPath = 173 String.format( 174 "%s%s%s", 175 ((IFolderBuildInfo) mBuildInfo).getRootDir().getAbsolutePath(), 176 File.separator, 177 apk); 178 mCmdArgs.add(apkPath); 179 } 180 // Unset potential build environment to ensure they do not affect the unit tests 181 getRunUtil().unsetEnvVariable(EnvVariable.ANDROID_HOST_OUT_TESTCASES.name()); 182 getRunUtil().unsetEnvVariable(EnvVariable.ANDROID_TARGET_OUT_TESTCASES.name()); 183 } 184 185 /** {@inheritDoc} */ 186 @Override postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)187 protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) { 188 super.postRun(listener, exception, elapsedTime); 189 reportMetrics(elapsedTime, listener); 190 if (mEnableHprof) { 191 logHprofResults(mHprofFile, listener); 192 } 193 194 if (mTmpDir != null) { 195 testTmpDirClean(mTmpDir, listener); 196 } 197 cleanTmpFile(); 198 } 199 200 @VisibleForTesting cleanTmpFile()201 void cleanTmpFile() { 202 FileUtil.deleteFile(mHprofFile); 203 } 204 205 /** 206 * Report an elapsed-time metric to keep track of it. 207 * 208 * @param elapsedTime time it took the subprocess to run. 209 * @param listener the {@link ITestInvocationListener} where to report the metric. 210 */ reportMetrics(long elapsedTime, ITestInvocationListener listener)211 private void reportMetrics(long elapsedTime, ITestInvocationListener listener) { 212 if (elapsedTime == -1L) { 213 return; 214 } 215 listener.testRunStarted("elapsed-time", 1); 216 TestDescription tid = new TestDescription("elapsed-time", "run-elapsed-time"); 217 listener.testStarted(tid); 218 HashMap<String, Metric> runMetrics = new HashMap<>(); 219 runMetrics.put( 220 "elapsed-time", TfMetricProtoUtil.stringToMetric(Long.toString(elapsedTime))); 221 listener.testEnded(tid, runMetrics); 222 listener.testRunEnded(0L, runMetrics); 223 } 224 225 /** 226 * Extra test to ensure no files are created by the unit tests in the subprocess and not 227 * cleaned. 228 * 229 * @param tmpDir the temporary dir of the subprocess. 230 * @param listener the {@link ITestInvocationListener} where to report the test. 231 */ 232 @VisibleForTesting testTmpDirClean(File tmpDir, ITestInvocationListener listener)233 protected void testTmpDirClean(File tmpDir, ITestInvocationListener listener) { 234 if (mSkipTmpDirCheck) { 235 return; 236 } 237 listener.testRunStarted("temporaryFiles", 1); 238 TestDescription tid = new TestDescription("temporary-files", "testIfClean"); 239 listener.testStarted(tid); 240 String[] listFiles = tmpDir.list(); 241 List<String> unmatchedFiles = new ArrayList<String>(); 242 List<String> patterns = new ArrayList<String>(Arrays.asList(EXPECTED_TMP_FILE_PATTERNS)); 243 patterns.add(mBuildInfo.getBuildBranch()); 244 for (String file : Arrays.asList(listFiles)) { 245 boolean matchFound = false; 246 for (String pattern : patterns) { 247 if (Pattern.matches(pattern, file)) { 248 patterns.remove(pattern); 249 matchFound = true; 250 break; 251 } 252 } 253 if (!matchFound) { 254 unmatchedFiles.add(file); 255 } 256 } 257 if (unmatchedFiles.size() > 0) { 258 String trace = 259 String.format( 260 "Found '%d' unexpected temporary files: %s.\nOnly " 261 + "expected files are: %s. And each should appears only once.", 262 unmatchedFiles.size(), unmatchedFiles, patterns); 263 listener.testFailed(tid, FailureDescription.create(trace)); 264 } 265 listener.testEnded(tid, new HashMap<String, Metric>()); 266 listener.testRunEnded(0, new HashMap<String, Metric>()); 267 } 268 269 /** 270 * Helper to log and report as metric the hprof data. 271 * 272 * @param hprofFile file containing the Hprof report 273 * @param listener the {@link ITestInvocationListener} where to report the test. 274 */ logHprofResults(File hprofFile, ITestInvocationListener listener)275 private void logHprofResults(File hprofFile, ITestInvocationListener listener) { 276 if (hprofFile == null) { 277 CLog.w("Hprof file was null. Skipping parsing."); 278 return; 279 } 280 if (!hprofFile.exists()) { 281 CLog.w("Hprof file %s was not found. Skipping parsing.", hprofFile.getAbsolutePath()); 282 return; 283 } 284 InputStreamSource memory = null; 285 File tmpGzip = null; 286 try { 287 tmpGzip = TarUtil.gzip(hprofFile); 288 memory = new FileInputStreamSource(tmpGzip); 289 listener.testLog("hprof", LogDataType.GZIP, memory); 290 } catch (IOException e) { 291 CLog.e(e); 292 return; 293 } finally { 294 StreamUtil.cancel(memory); 295 FileUtil.deleteFile(tmpGzip); 296 } 297 HprofAllocSiteParser parser = new HprofAllocSiteParser(); 298 try { 299 Map<String, String> results = parser.parse(hprofFile); 300 if (results.isEmpty()) { 301 CLog.d("No allocation site found from hprof file"); 302 return; 303 } 304 listener.testRunStarted("hprofAllocSites", 1); 305 TestDescription tid = new TestDescription("hprof", "allocationSites"); 306 listener.testStarted(tid); 307 listener.testEnded(tid, TfMetricProtoUtil.upgradeConvert(results)); 308 listener.testRunEnded(0, new HashMap<String, Metric>()); 309 } catch (IOException e) { 310 throw new RuntimeException(e); 311 } 312 } 313 } 314