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.IBuildInfo; 19 import com.android.tradefed.build.IFolderBuildInfo; 20 import com.android.tradefed.command.CommandOptions; 21 import com.android.tradefed.config.GlobalConfiguration; 22 import com.android.tradefed.config.IConfiguration; 23 import com.android.tradefed.config.IConfigurationReceiver; 24 import com.android.tradefed.config.Option; 25 import com.android.tradefed.config.proxy.AutomatedReporters; 26 import com.android.tradefed.device.DeviceNotAvailableException; 27 import com.android.tradefed.error.HarnessRuntimeException; 28 import com.android.tradefed.invoker.DelegatedInvocationExecution; 29 import com.android.tradefed.invoker.IInvocationContext; 30 import com.android.tradefed.invoker.RemoteInvocationExecution; 31 import com.android.tradefed.invoker.TestInformation; 32 import com.android.tradefed.log.LogUtil.CLog; 33 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 34 import com.android.tradefed.result.FailureDescription; 35 import com.android.tradefed.result.FileInputStreamSource; 36 import com.android.tradefed.result.ITestInvocationListener; 37 import com.android.tradefed.result.LogDataType; 38 import com.android.tradefed.result.TestDescription; 39 import com.android.tradefed.result.error.InfraErrorIdentifier; 40 import com.android.tradefed.result.proto.StreamProtoReceiver; 41 import com.android.tradefed.result.proto.StreamProtoResultReporter; 42 import com.android.tradefed.service.TradefedFeatureServer; 43 import com.android.tradefed.util.CommandResult; 44 import com.android.tradefed.util.CommandStatus; 45 import com.android.tradefed.util.FileUtil; 46 import com.android.tradefed.util.IRunUtil; 47 import com.android.tradefed.util.IRunUtil.EnvPriority; 48 import com.android.tradefed.util.RunUtil; 49 import com.android.tradefed.util.StreamUtil; 50 import com.android.tradefed.util.StringEscapeUtils; 51 import com.android.tradefed.util.SubprocessExceptionParser; 52 import com.android.tradefed.util.SubprocessTestResultsParser; 53 import com.android.tradefed.util.SystemUtil; 54 import com.android.tradefed.util.TimeUtil; 55 import com.android.tradefed.util.UniqueMultiMap; 56 57 import org.junit.Assert; 58 59 import java.io.File; 60 import java.io.FileOutputStream; 61 import java.io.IOException; 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.HashMap; 65 import java.util.LinkedHashSet; 66 import java.util.List; 67 import java.util.Set; 68 69 /** 70 * A {@link IRemoteTest} for running tests against a separate TF installation. 71 * 72 * <p>Launches an external java process to run the tests. Used for running the TF unit or functional 73 * tests continuously. 74 */ 75 public abstract class SubprocessTfLauncher 76 implements IBuildReceiver, IInvocationContextReceiver, IRemoteTest, IConfigurationReceiver { 77 78 /** The tag that will be passed to the TF subprocess to differentiate it */ 79 public static final String SUBPROCESS_TAG_NAME = "subprocess"; 80 81 public static final String PARENT_PROC_TAG_NAME = "parentprocess"; 82 /** Env. variable that affects adb selection. */ 83 public static final String ANDROID_SERIAL_VAR = "ANDROID_SERIAL"; 84 85 @Option(name = "max-run-time", description = 86 "The maximum time to allow for a TF test run.", isTimeVal = true) 87 private long mMaxTfRunTime = 20 * 60 * 1000; 88 89 @Option(name = "remote-debug", description = 90 "Start the TF java process in remote debug mode.") 91 private boolean mRemoteDebug = false; 92 93 @Option(name = "config-name", description = "The config that runs the TF tests") 94 private String mConfigName; 95 96 @Option( 97 name = "local-sharding-mode", 98 description = 99 "If sharding is requested, allow the launcher to run with local sharding.") 100 private boolean mLocalShardingMode = false; 101 102 @Option(name = "use-event-streaming", description = "Use a socket to receive results as they" 103 + "arrived instead of using a temporary file and parsing at the end.") 104 private boolean mEventStreaming = true; 105 106 @Option( 107 name = "use-proto-reporting", 108 description = "Use a proto result reporter for the results from the subprocess.") 109 private boolean mUseProtoReporting = true; 110 111 @Option(name = "sub-global-config", description = "The global config name to pass to the" 112 + "sub process, can be local or from jar resources. Be careful of conflicts with " 113 + "parent process.") 114 private String mGlobalConfig = null; 115 116 @Option( 117 name = "inject-invocation-data", 118 description = "Pass the invocation-data to the subprocess if enabled.") 119 private boolean mInjectInvocationData = true; 120 121 @Option(name = "ignore-test-log", description = "Only rely on logAssociation for logs.") 122 private boolean mIgnoreTestLog = true; 123 124 @Option( 125 name = "disable-stderr-test", 126 description = "Whether or not to disable the stderr validation check." 127 ) 128 private boolean mDisableStderrTest = false; 129 130 @Option( 131 name = "disable-add-opens", 132 description = "Whether or not to add the java add-opens flags" 133 ) 134 private boolean mDisableJavaOpens = false; 135 136 @Option(name = "add-opens", description = "Whether or not to add the java add-opens flags") 137 private Set<String> mAddOpens = 138 new LinkedHashSet<>( 139 Arrays.asList( 140 "java.base/java.nio", 141 "java.base/sun.reflect.annotation", 142 "java.base/java.io")); 143 144 // Represents all the args to be passed to the sub process 145 @Option(name = "sub-params", description = "Parameters to feed the subprocess.") 146 private List<String> mSubParams = new ArrayList<String>(); 147 148 // Temp global configuration filtered from the parent process. 149 private String mFilteredGlobalConfig = null; 150 151 private static final List<String> TRADEFED_JARS = 152 new ArrayList<>( 153 Arrays.asList( 154 // Loganalysis 155 "loganalysis.jar", 156 "loganalysis-tests.jar", 157 // Aosp Tf jars 158 "tradefed.jar", 159 "tradefed-tests.jar", 160 // libs 161 "tools-common-prebuilt.jar", 162 // jar in older branches 163 "tf-prod-tests.jar", 164 "tf-prod-metatests.jar", 165 // Aosp contrib jars 166 "tradefed-contrib.jar", 167 "tf-contrib-tests.jar", 168 // Google Tf jars 169 "google-tf-prod-tests.jar", 170 "google-tf-prod-metatests.jar", 171 "google-tradefed.jar", 172 "google-tradefed-tests.jar", 173 // Google contrib jars 174 "google-tradefed-contrib.jar", 175 // Older jar required for coverage tests 176 "jack-jacoco-reporter.jar", 177 "emmalib.jar")); 178 179 /** Timeout to wait for the events received from subprocess to finish being processed.*/ 180 private static final long EVENT_THREAD_JOIN_TIMEOUT_MS = 30 * 1000; 181 182 protected IRunUtil mRunUtil = new RunUtil(); 183 184 protected IBuildInfo mBuildInfo = null; 185 // Temp directory to run the TF process. 186 protected File mTmpDir = null; 187 // List of command line arguments to run the TF process. 188 protected List<String> mCmdArgs = null; 189 // The absolute path to the build's root directory. 190 protected String mRootDir = null; 191 protected IConfiguration mConfig; 192 private IInvocationContext mContext; 193 194 @Override setInvocationContext(IInvocationContext invocationContext)195 public void setInvocationContext(IInvocationContext invocationContext) { 196 mContext = invocationContext; 197 } 198 199 @Override setConfiguration(IConfiguration configuration)200 public void setConfiguration(IConfiguration configuration) { 201 mConfig = configuration; 202 } 203 setProtoReporting(boolean protoReporting)204 protected void setProtoReporting(boolean protoReporting) { 205 mUseProtoReporting = protoReporting; 206 } 207 208 /** 209 * Set use-event-streaming. 210 * 211 * Exposed for unit testing. 212 */ setEventStreaming(boolean eventStreaming)213 protected void setEventStreaming(boolean eventStreaming) { 214 mEventStreaming = eventStreaming; 215 } 216 217 /** 218 * Set IRunUtil. 219 * 220 * Exposed for unit testing. 221 */ setRunUtil(IRunUtil runUtil)222 protected void setRunUtil(IRunUtil runUtil) { 223 mRunUtil = runUtil; 224 } 225 226 /** Returns the {@link IRunUtil} that will be used for the subprocess command. */ getRunUtil()227 protected IRunUtil getRunUtil() { 228 return mRunUtil; 229 } 230 231 /** 232 * Setup before running the test. 233 */ preRun()234 protected void preRun() { 235 Assert.assertNotNull(mBuildInfo); 236 Assert.assertNotNull(mConfigName); 237 IFolderBuildInfo tfBuild = (IFolderBuildInfo) mBuildInfo; 238 File rootDirFile = tfBuild.getRootDir(); 239 mRootDir = rootDirFile.getAbsolutePath(); 240 String jarClasspath = ""; 241 List<String> paths = new ArrayList<>(); 242 for (String jar : TRADEFED_JARS) { 243 File f = FileUtil.findFile(rootDirFile, jar); 244 if (f != null && f.exists()) { 245 paths.add(f.getAbsolutePath()); 246 } 247 } 248 jarClasspath = String.join(":", paths); 249 250 mCmdArgs = new ArrayList<String>(); 251 mCmdArgs.add(getJava()); 252 253 try { 254 mTmpDir = FileUtil.createTempDir("subprocess-" + tfBuild.getBuildId()); 255 mCmdArgs.add(String.format("-Djava.io.tmpdir=%s", mTmpDir.getAbsolutePath())); 256 } catch (IOException e) { 257 CLog.e(e); 258 throw new RuntimeException(e); 259 } 260 261 addJavaArguments(mCmdArgs); 262 263 if (mRemoteDebug) { 264 mCmdArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10088"); 265 } 266 // This prevent the illegal reflective access warnings by allowing some packages. 267 if (!mDisableJavaOpens) { 268 for (String modulePackage : mAddOpens) { 269 mCmdArgs.add("--add-opens=" + modulePackage + "=ALL-UNNAMED"); 270 } 271 } 272 mCmdArgs.add("-cp"); 273 274 mCmdArgs.add(jarClasspath); 275 mCmdArgs.add("com.android.tradefed.command.CommandRunner"); 276 mCmdArgs.add(mConfigName); 277 278 Integer shardCount = mConfig.getCommandOptions().getShardCount(); 279 if (mLocalShardingMode && shardCount != null & shardCount > 1) { 280 mCmdArgs.add("--shard-count"); 281 mCmdArgs.add(Integer.toString(shardCount)); 282 } 283 284 if (!mSubParams.isEmpty()) { 285 mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mSubParams)); 286 } 287 288 // clear the TF_GLOBAL_CONFIG env, so another tradefed will not reuse the global config file 289 mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE); 290 mRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE); 291 mRunUtil.unsetEnvVariable(ANDROID_SERIAL_VAR); 292 mRunUtil.unsetEnvVariable(DelegatedInvocationExecution.DELEGATED_MODE_VAR); 293 for (String variable : AutomatedReporters.REPORTER_MAPPING) { 294 mRunUtil.unsetEnvVariable(variable); 295 } 296 // Handle feature server 297 getRunUtil().unsetEnvVariable(RemoteInvocationExecution.START_FEATURE_SERVER); 298 getRunUtil().unsetEnvVariable(TradefedFeatureServer.TF_SERVICE_PORT); 299 getRunUtil().setEnvVariablePriority(EnvPriority.SET); 300 getRunUtil() 301 .setEnvVariable( 302 TradefedFeatureServer.TF_SERVICE_PORT, 303 Integer.toString(TradefedFeatureServer.getPort())); 304 305 if (mGlobalConfig == null) { 306 // If the global configuration is not set in option, create a filtered global 307 // configuration for subprocess to use. 308 try { 309 File filteredGlobalConfig = 310 GlobalConfiguration.getInstance().cloneConfigWithFilter(); 311 mFilteredGlobalConfig = filteredGlobalConfig.getAbsolutePath(); 312 mGlobalConfig = mFilteredGlobalConfig; 313 } catch (IOException e) { 314 CLog.e("Failed to create filtered global configuration"); 315 CLog.e(e); 316 } 317 } 318 if (mGlobalConfig != null) { 319 // We allow overriding this global config and then set it for the subprocess. 320 mRunUtil.setEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE, mGlobalConfig); 321 } 322 } 323 324 /** 325 * Allow to add extra java parameters to the subprocess invocation. 326 * 327 * @param args the current list of arguments to which we need to add the extra ones. 328 */ addJavaArguments(List<String> args)329 protected void addJavaArguments(List<String> args) {} 330 331 /** 332 * Actions to take after the TF test is finished. 333 * 334 * @param listener the original {@link ITestInvocationListener} where to report results. 335 * @param exception True if exception was raised inside the test. 336 * @param elapsedTime the time taken to run the tests. 337 */ postRun(ITestInvocationListener listener, boolean exception, long elapsedTime)338 protected void postRun(ITestInvocationListener listener, boolean exception, long elapsedTime) {} 339 340 /** Pipe to the subprocess the invocation-data so that it can use them if needed. */ addInvocationData()341 private void addInvocationData() { 342 if (!mInjectInvocationData) { 343 return; 344 } 345 UniqueMultiMap<String, String> data = mConfig.getCommandOptions().getInvocationData(); 346 for (String key : data.keySet()) { 347 for (String value : data.get(key)) { 348 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA); 349 mCmdArgs.add(key); 350 mCmdArgs.add(value); 351 } 352 } 353 // Finally add one last more to tag the subprocess 354 mCmdArgs.add("--" + CommandOptions.INVOCATION_DATA); 355 mCmdArgs.add(SUBPROCESS_TAG_NAME); 356 mCmdArgs.add("true"); 357 // Tag the parent invocation 358 mBuildInfo.addBuildAttribute(PARENT_PROC_TAG_NAME, "true"); 359 } 360 361 /** {@inheritDoc} */ 362 @Override run(TestInformation testInfo, ITestInvocationListener listener)363 public void run(TestInformation testInfo, ITestInvocationListener listener) 364 throws DeviceNotAvailableException { 365 preRun(); 366 addInvocationData(); 367 368 File stdoutFile = null; 369 File stderrFile = null; 370 File eventFile = null; 371 SubprocessTestResultsParser eventParser = null; 372 StreamProtoReceiver protoReceiver = null; 373 FileOutputStream stdout = null; 374 FileOutputStream stderr = null; 375 376 boolean exception = false; 377 long startTime = 0L; 378 long elapsedTime = -1L; 379 try { 380 stdoutFile = FileUtil.createTempFile("stdout_subprocess_", ".log"); 381 stderrFile = FileUtil.createTempFile("stderr_subprocess_", ".log"); 382 stderr = new FileOutputStream(stderrFile); 383 stdout = new FileOutputStream(stdoutFile); 384 if (mUseProtoReporting) { 385 // Skip merging properties to avoid contaminating metrics with unit tests 386 protoReceiver = 387 new StreamProtoReceiver( 388 listener, mContext, false, false, true, "subprocess-", false); 389 mCmdArgs.add("--" + StreamProtoResultReporter.PROTO_REPORT_PORT_OPTION); 390 mCmdArgs.add(Integer.toString(protoReceiver.getSocketServerPort())); 391 } else { 392 eventParser = new SubprocessTestResultsParser(listener, mEventStreaming, mContext); 393 if (mEventStreaming) { 394 mCmdArgs.add("--subprocess-report-port"); 395 mCmdArgs.add(Integer.toString(eventParser.getSocketServerPort())); 396 } else { 397 eventFile = FileUtil.createTempFile("event_subprocess_", ".log"); 398 mCmdArgs.add("--subprocess-report-file"); 399 mCmdArgs.add(eventFile.getAbsolutePath()); 400 } 401 eventParser.setIgnoreTestLog(mIgnoreTestLog); 402 } 403 startTime = System.currentTimeMillis(); 404 CommandResult result = mRunUtil.runTimedCmd(mMaxTfRunTime, stdout, 405 stderr, mCmdArgs.toArray(new String[0])); 406 407 if (eventParser != null) { 408 if (eventParser.getStartTime() != null) { 409 startTime = eventParser.getStartTime(); 410 } 411 elapsedTime = System.currentTimeMillis() - startTime; 412 // We possibly allow for a little more time if the thread is still processing 413 // events. 414 if (!eventParser.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) { 415 elapsedTime = -1L; 416 throw new RuntimeException( 417 String.format( 418 "Event receiver thread did not complete:" + "\n%s", 419 FileUtil.readStringFromFile(stderrFile))); 420 } 421 } else if (protoReceiver != null) { 422 if (!protoReceiver.joinReceiver(EVENT_THREAD_JOIN_TIMEOUT_MS)) { 423 elapsedTime = -1L; 424 throw new RuntimeException( 425 String.format( 426 "Event receiver thread did not complete:" + "\n%s", 427 FileUtil.readStringFromFile(stderrFile))); 428 } 429 protoReceiver.completeModuleEvents(); 430 } 431 if (result.getStatus().equals(CommandStatus.SUCCESS)) { 432 CLog.d("Successfully ran TF tests for build %s", mBuildInfo.getBuildId()); 433 testCleanStdErr(stderrFile, listener); 434 } else { 435 CLog.w("Failed ran TF tests for build %s, status %s", 436 mBuildInfo.getBuildId(), result.getStatus()); 437 CLog.v( 438 "TF tests output:\nstdout:\n%s\nstderr:\n%s", 439 result.getStdout(), result.getStderr()); 440 exception = true; 441 String errMessage = null; 442 if (result.getStatus().equals(CommandStatus.TIMED_OUT)) { 443 errMessage = String.format("Timeout after %s", 444 TimeUtil.formatElapsedTime(mMaxTfRunTime)); 445 throw new HarnessRuntimeException( 446 String.format( 447 "%s Tests subprocess failed due to:\n%s\n", 448 mConfigName, errMessage), 449 InfraErrorIdentifier.INVOCATION_TIMEOUT); 450 } else if (eventParser != null && !eventParser.reportedInvocationFailed()) { 451 SubprocessExceptionParser.handleStderrException(result); 452 } 453 } 454 } catch (IOException e) { 455 exception = true; 456 throw new RuntimeException(e); 457 } finally { 458 StreamUtil.close(stdout); 459 StreamUtil.close(stderr); 460 logAndCleanFile(stdoutFile, listener); 461 logAndCleanFile(stderrFile, listener); 462 if (eventFile != null) { 463 eventParser.parseFile(eventFile); 464 logAndCleanFile(eventFile, listener); 465 } 466 StreamUtil.close(eventParser); 467 StreamUtil.close(protoReceiver); 468 469 if (mGlobalConfig != null) { 470 logAndCleanFile(new File(mGlobalConfig), listener); 471 } 472 473 postRun(listener, exception, elapsedTime); 474 475 if (mTmpDir != null) { 476 FileUtil.recursiveDelete(mTmpDir); 477 } 478 479 if (mFilteredGlobalConfig != null) { 480 FileUtil.deleteFile(new File(mFilteredGlobalConfig)); 481 } 482 } 483 } 484 485 /** 486 * Log the content of given file to listener, then remove the file. 487 * 488 * @param fileToExport the {@link File} pointing to the file to log. 489 * @param listener the {@link ITestInvocationListener} where to report the test. 490 */ logAndCleanFile(File fileToExport, ITestInvocationListener listener)491 private void logAndCleanFile(File fileToExport, ITestInvocationListener listener) { 492 if (fileToExport == null) { 493 return; 494 } 495 496 try (FileInputStreamSource inputStream = new FileInputStreamSource(fileToExport)) { 497 listener.testLog(fileToExport.getName(), LogDataType.TEXT, inputStream); 498 } 499 FileUtil.deleteFile(fileToExport); 500 } 501 502 /** 503 * {@inheritDoc} 504 */ 505 @Override setBuild(IBuildInfo buildInfo)506 public void setBuild(IBuildInfo buildInfo) { 507 mBuildInfo = buildInfo; 508 } 509 510 /** 511 * Extra test to ensure no abnormal logging is made to stderr when all the tests pass. 512 * 513 * @param stdErrFile the stderr log file of the subprocess. 514 * @param listener the {@link ITestInvocationListener} where to report the test. 515 */ testCleanStdErr(File stdErrFile, ITestInvocationListener listener)516 private void testCleanStdErr(File stdErrFile, ITestInvocationListener listener) 517 throws IOException { 518 if (mDisableStderrTest) { 519 return; 520 } 521 listener.testRunStarted("StdErr", 1); 522 TestDescription tid = new TestDescription("stderr-test", "checkIsEmpty"); 523 listener.testStarted(tid); 524 if (!FileUtil.readStringFromFile(stdErrFile).isEmpty()) { 525 String trace = 526 String.format( 527 "Found some output in stderr:\n%s", 528 FileUtil.readStringFromFile(stdErrFile)); 529 listener.testFailed(tid, FailureDescription.create(trace)); 530 } 531 listener.testEnded(tid, new HashMap<String, Metric>()); 532 listener.testRunEnded(0, new HashMap<String, Metric>()); 533 } 534 getJava()535 protected String getJava() { 536 return SystemUtil.getRunningJavaBinaryPath().getAbsolutePath(); 537 } 538 } 539