1 /*
2  * Copyright (C) 2017 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.python;
17 
18 import com.android.annotations.VisibleForTesting;
19 import com.android.tradefed.build.IBuildInfo;
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.ITestDevice;
25 import com.android.tradefed.device.StubDevice;
26 import com.android.tradefed.invoker.IInvocationContext;
27 import com.android.tradefed.log.LogUtil.CLog;
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.ResultForwarder;
32 import com.android.tradefed.testtype.IBuildReceiver;
33 import com.android.tradefed.testtype.IDeviceTest;
34 import com.android.tradefed.testtype.IInvocationContextReceiver;
35 import com.android.tradefed.testtype.IRemoteTest;
36 import com.android.tradefed.util.CommandResult;
37 import com.android.tradefed.util.CommandStatus;
38 import com.android.tradefed.util.FileUtil;
39 import com.android.tradefed.util.IRunUtil;
40 import com.android.tradefed.util.RunUtil;
41 import com.android.tradefed.util.StreamUtil;
42 import com.android.tradefed.util.SubprocessTestResultsParser;
43 
44 import java.io.File;
45 import java.io.IOException;
46 import java.util.ArrayList;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Set;
50 
51 /** Host test meant to run a python binary file from the Android Build system (Soong) */
52 @OptionClass(alias = "python-host")
53 public class PythonBinaryHostTest
54         implements IRemoteTest, IDeviceTest, IBuildReceiver, IInvocationContextReceiver {
55 
56     protected static final String PYTHON_OUTPUT = "python-output";
57 
58     @Option(name = "par-file-name", description = "The binary names inside the build info to run.")
59     private Set<String> mBinaryNames = new HashSet<>();
60 
61     @Option(
62         name = "python-binaries",
63         description = "The full path to a runnable python binary. Can be repeated."
64     )
65     private Set<File> mBinaries = new HashSet<>();
66 
67     @Option(
68         name = "test-timeout",
69         description = "Timeout for a single par file to terminate.",
70         isTimeVal = true
71     )
72     private long mTestTimeout = 20 * 1000L;
73 
74     private ITestDevice mDevice;
75     private IBuildInfo mBuildInfo;
76     private IInvocationContext mContext;
77 
78     @Override
setDevice(ITestDevice device)79     public void setDevice(ITestDevice device) {
80         mDevice = device;
81     }
82 
83     @Override
getDevice()84     public ITestDevice getDevice() {
85         return mDevice;
86     }
87 
88     @Override
setBuild(IBuildInfo buildInfo)89     public void setBuild(IBuildInfo buildInfo) {
90         mBuildInfo = buildInfo;
91     }
92 
93     @Override
setInvocationContext(IInvocationContext invocationContext)94     public void setInvocationContext(IInvocationContext invocationContext) {
95         mContext = invocationContext;
96     }
97 
98     @Override
run(ITestInvocationListener listener)99     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
100         List<File> pythonFilesList = findParFiles();
101         for (File pyFile : pythonFilesList) {
102             if (!pyFile.exists()) {
103                 CLog.d(
104                         "ignoring %s which doesn't look like a test file.",
105                         pyFile.getAbsolutePath());
106                 continue;
107             }
108             pyFile.setExecutable(true);
109             runSinglePythonFile(listener, pyFile);
110         }
111     }
112 
findParFiles()113     private List<File> findParFiles() {
114         File testsDir = null;
115         if (mBuildInfo instanceof IDeviceBuildInfo) {
116             testsDir = ((IDeviceBuildInfo) mBuildInfo).getTestsDir();
117         }
118         List<File> files = new ArrayList<>();
119         for (String parFileName : mBinaryNames) {
120             File res = null;
121             // search tests dir
122             if (testsDir != null) {
123                 res = FileUtil.findFile(testsDir, parFileName);
124             }
125 
126             // TODO: is there other places to search?
127             if (res == null) {
128                 throw new RuntimeException(
129                         String.format("Couldn't find a par file %s", parFileName));
130             }
131             files.add(res);
132         }
133         files.addAll(mBinaries);
134         return files;
135     }
136 
runSinglePythonFile(ITestInvocationListener listener, File pyFile)137     private void runSinglePythonFile(ITestInvocationListener listener, File pyFile) {
138         List<String> commandLine = new ArrayList<>();
139         commandLine.add(pyFile.getAbsolutePath());
140         // Run with -q (quiet) to avoid extraneous outputs
141         commandLine.add("-q");
142         // If we have a physical device, pass it to the python test by serial
143         if (!(getDevice().getIDevice() instanceof StubDevice)) {
144             // TODO: support multi-device python tests?
145             commandLine.add("-s");
146             commandLine.add(getDevice().getSerialNumber());
147         }
148         CommandResult result =
149                 getRunUtil().runTimedCmd(mTestTimeout, commandLine.toArray(new String[0]));
150         PythonForwarder forwarder = new PythonForwarder(listener, pyFile.getName());
151         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
152             // If the binary finishes we an error code, it could simply be a test failure, but if
153             // it does not even have a TEST_RUN_STARTED tag, then we probably have binary setup
154             // issue.
155             if (!result.getStderr().contains("TEST_RUN_STARTED")) {
156                 throw new RuntimeException(
157                         String.format(
158                                 "Something went wrong when running the python binary: %s",
159                                 result.getStderr()));
160             }
161         }
162         SubprocessTestResultsParser parser = new SubprocessTestResultsParser(forwarder, mContext);
163         File resultFile = null;
164         try {
165             resultFile = FileUtil.createTempFile("python-res", ".txt");
166             FileUtil.writeToFile(result.getStderr(), resultFile);
167             try (FileInputStreamSource data = new FileInputStreamSource(resultFile)) {
168                 listener.testLog(PYTHON_OUTPUT, LogDataType.TEXT, data);
169             }
170             parser.parseFile(resultFile);
171         } catch (IOException e) {
172             throw new RuntimeException(e);
173         } finally {
174             FileUtil.deleteFile(resultFile);
175             StreamUtil.close(parser);
176         }
177     }
178 
179     @VisibleForTesting
getRunUtil()180     IRunUtil getRunUtil() {
181         return RunUtil.getDefault();
182     }
183 
184     /** Result forwarder to replace the run name by the binary name. */
185     public class PythonForwarder extends ResultForwarder {
186 
187         private String mRunName;
188 
189         /** Ctor with the run name using the binary name. */
PythonForwarder(ITestInvocationListener listener, String name)190         public PythonForwarder(ITestInvocationListener listener, String name) {
191             super(listener);
192             mRunName = name;
193         }
194 
195         @Override
testRunStarted(String runName, int testCount)196         public void testRunStarted(String runName, int testCount) {
197             // Replace run name
198             super.testRunStarted(mRunName, testCount);
199         }
200     }
201 }
202