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