1 /*
2  * Copyright (C) 2019 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.binary;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
20 import com.android.tradefed.build.IDeviceBuildInfo;
21 import com.android.tradefed.config.Option;
22 import com.android.tradefed.config.OptionClass;
23 import com.android.tradefed.device.DeviceNotAvailableException;
24 import com.android.tradefed.device.StubDevice;
25 import com.android.tradefed.log.ITestLogger;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.result.FailureDescription;
28 import com.android.tradefed.result.FileInputStreamSource;
29 import com.android.tradefed.result.ITestInvocationListener;
30 import com.android.tradefed.result.LogDataType;
31 import com.android.tradefed.result.TestDescription;
32 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus;
33 import com.android.tradefed.testtype.IDeviceTest;
34 import com.android.tradefed.util.CommandResult;
35 import com.android.tradefed.util.CommandStatus;
36 import com.android.tradefed.util.FileUtil;
37 import com.android.tradefed.util.IRunUtil;
38 import com.android.tradefed.util.RunUtil;
39 
40 import java.io.File;
41 import java.io.FileOutputStream;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 /**
47  * Test runner for executable running on the host. The runner implements {@link IDeviceTest} since
48  * the host binary might communicate to a device. If the received device is not a {@link StubDevice}
49  * the serial will be passed to the binary to be used.
50  */
51 @OptionClass(alias = "executable-host-test")
52 public class ExecutableHostTest extends ExecutableBaseTest {
53 
54     private static final String ANDROID_SERIAL = "ANDROID_SERIAL";
55     private static final String LOG_STDOUT_TAG = "-binary-stdout-";
56     private static final String LOG_STDERR_TAG = "-binary-stderr-";
57 
58     @Option(
59         name = "relative-path-execution",
60         description =
61                 "Some scripts assume a relative location to their tests file, this allows to"
62                         + " execute with that relative location."
63     )
64     private boolean mExecuteRelativeToScript = false;
65 
66     @Override
findBinary(String binary)67     public String findBinary(String binary) {
68         File bin = new File(binary);
69         // If it's a local path or absolute path
70         if (bin.exists()) {
71             return bin.getAbsolutePath();
72         }
73         if (getTestInfo().getBuildInfo() instanceof IDeviceBuildInfo) {
74             IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) getTestInfo().getBuildInfo();
75             File testsDir = deviceBuild.getTestsDir();
76 
77             List<File> scanDirs = new ArrayList<>();
78             // If it exists, always look first in the ANDROID_HOST_OUT_TESTCASES
79             File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.HOST_LINKED_DIR);
80             if (targetTestCases != null) {
81                 scanDirs.add(targetTestCases);
82             }
83             if (testsDir != null) {
84                 scanDirs.add(testsDir);
85             }
86 
87             try {
88                 // Search the full tests dir if no target dir is available.
89                 File src = FileUtil.findFile(binary, getAbi(), scanDirs.toArray(new File[] {}));
90                 if (src != null) {
91                     return src.getAbsolutePath();
92                 }
93             } catch (IOException e) {
94                 CLog.e("Failed to find test files from directory.");
95             }
96         }
97         return null;
98     }
99 
100     @Override
runBinary( String binaryPath, ITestInvocationListener listener, TestDescription description)101     public void runBinary(
102             String binaryPath, ITestInvocationListener listener, TestDescription description)
103             throws DeviceNotAvailableException, IOException {
104         IRunUtil runUtil = createRunUtil();
105         // Output everything in stdout
106         runUtil.setRedirectStderrToStdout(true);
107         // If we are running against a real device, set ANDROID_SERIAL to the proper serial.
108         if (!(getTestInfo().getDevice().getIDevice() instanceof StubDevice)) {
109             runUtil.setEnvVariable(ANDROID_SERIAL, getTestInfo().getDevice().getSerialNumber());
110         }
111         // Ensure its executable
112         FileUtil.chmodRWXRecursively(new File(binaryPath));
113 
114         List<String> command = new ArrayList<>();
115         String scriptName = new File(binaryPath).getName();
116         if (mExecuteRelativeToScript) {
117             String parentDir = new File(binaryPath).getParent();
118             command.add("bash");
119             command.add("-c");
120             command.add(String.format("pushd %s; ./%s;", parentDir, scriptName));
121         } else {
122             command.add(binaryPath);
123         }
124         File stdout = FileUtil.createTempFile(scriptName + LOG_STDOUT_TAG, ".txt");
125         File stderr = FileUtil.createTempFile(scriptName + LOG_STDERR_TAG, ".txt");
126 
127         try (FileOutputStream stdoutStream = new FileOutputStream(stdout);
128                 FileOutputStream stderrStream = new FileOutputStream(stderr); ) {
129             CommandResult res =
130                     runUtil.runTimedCmd(
131                             getTimeoutPerBinaryMs(),
132                             stdoutStream,
133                             stderrStream,
134                             command.toArray(new String[0]));
135             if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
136                 FailureStatus status = FailureStatus.TEST_FAILURE;
137                 // Everything should be outputted in stdout with our redirect above.
138                 String errorMessage = FileUtil.readStringFromFile(stdout);
139                 if (CommandStatus.TIMED_OUT.equals(res.getStatus())) {
140                     errorMessage += "\nTimeout.";
141                     status = FailureStatus.TIMED_OUT;
142                 }
143                 if (res.getExitCode() != null) {
144                     errorMessage += String.format("\nExit Code: %s", res.getExitCode());
145                 }
146                 listener.testFailed(
147                         description,
148                         FailureDescription.create(errorMessage).setFailureStatus(status));
149             }
150         } finally {
151             logFile(stdout, listener);
152             logFile(stderr, listener);
153         }
154 
155         if (!(getTestInfo().getDevice().getIDevice() instanceof StubDevice)) {
156             // Ensure that the binary did not leave the device offline.
157             CLog.d("Checking whether device is still online after %s", binaryPath);
158             try {
159                 getTestInfo().getDevice().waitForDeviceAvailable();
160             } catch (DeviceNotAvailableException e) {
161                 listener.testRunFailed(
162                         String.format("Device became unavailable after %s.", binaryPath));
163                 throw e;
164             }
165         }
166     }
167 
168     @VisibleForTesting
createRunUtil()169     IRunUtil createRunUtil() {
170         return new RunUtil();
171     }
172 
logFile(File logFile, ITestLogger logger)173     private void logFile(File logFile, ITestLogger logger) {
174         try (FileInputStreamSource source = new FileInputStreamSource(logFile, true)) {
175             logger.testLog(logFile.getName(), LogDataType.TEXT, source);
176         }
177     }
178 }
179