1 /* 2 * Copyright (C) 2012 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 17 package com.android.commands.uiautomator; 18 19 import android.os.Bundle; 20 import android.util.Log; 21 22 import com.android.commands.uiautomator.Launcher.Command; 23 import com.android.uiautomator.testrunner.UiAutomatorTestRunner; 24 25 import dalvik.system.DexFile; 26 27 import java.io.IOException; 28 import java.util.ArrayList; 29 import java.util.Enumeration; 30 import java.util.List; 31 32 /** 33 * Implementation of the runtest sub command 34 * 35 */ 36 public class RunTestCommand extends Command { 37 private static final String LOGTAG = RunTestCommand.class.getSimpleName(); 38 39 private static final String OUTPUT_SIMPLE = "simple"; 40 private static final String OUTPUT_FORMAT_KEY = "outputFormat"; 41 private static final String CLASS_PARAM = "class"; 42 private static final String JARS_PARAM = "jars"; 43 private static final String DEBUG_PARAM = "debug"; 44 private static final String RUNNER_PARAM = "runner"; 45 private static final String CLASS_SEPARATOR = ","; 46 private static final String JARS_SEPARATOR = ":"; 47 private static final int ARG_OK = 0; 48 private static final int ARG_FAIL_INCOMPLETE_E = -1; 49 private static final int ARG_FAIL_INCOMPLETE_C = -2; 50 private static final int ARG_FAIL_NO_CLASS = -3; 51 private static final int ARG_FAIL_RUNNER = -4; 52 private static final int ARG_FAIL_UNSUPPORTED = -99; 53 54 private final Bundle mParams = new Bundle(); 55 private final List<String> mTestClasses = new ArrayList<String>(); 56 private boolean mDebug; 57 private boolean mMonkey = false; 58 private String mRunnerClassName; 59 private UiAutomatorTestRunner mRunner; 60 61 public RunTestCommand() { 62 super("runtest"); 63 } 64 65 @Override 66 public void run(String[] args) { 67 int ret = parseArgs(args); 68 switch (ret) { 69 case ARG_FAIL_INCOMPLETE_C: 70 System.err.println("Incomplete '-c' parameter."); 71 System.exit(ARG_FAIL_INCOMPLETE_C); 72 break; 73 case ARG_FAIL_INCOMPLETE_E: 74 System.err.println("Incomplete '-e' parameter."); 75 System.exit(ARG_FAIL_INCOMPLETE_E); 76 break; 77 case ARG_FAIL_UNSUPPORTED: 78 System.err.println("Unsupported standalone parameter."); 79 System.exit(ARG_FAIL_UNSUPPORTED); 80 break; 81 default: 82 break; 83 } 84 if (mTestClasses.isEmpty()) { 85 addTestClassesFromJars(); 86 if (mTestClasses.isEmpty()) { 87 System.err.println("No test classes found."); 88 System.exit(ARG_FAIL_NO_CLASS); 89 } 90 } 91 getRunner().run(mTestClasses, mParams, mDebug, mMonkey); 92 } 93 94 private int parseArgs(String[] args) { 95 // we are parsing for these parameters: 96 // -e <key> <value> 97 // key-value pairs 98 // special ones are: 99 // key is "class", parameter is passed onto JUnit as class name to run 100 // key is "debug", parameter will determine whether to wait for debugger 101 // to attach 102 // -c <class name> 103 // -s turns on the simple output format 104 // equivalent to -e class <class name>, i.e. passed onto JUnit 105 for (int i = 0; i < args.length; i++) { 106 if (args[i].equals("-e")) { 107 if (i + 2 < args.length) { 108 String key = args[++i]; 109 String value = args[++i]; 110 if (CLASS_PARAM.equals(key)) { 111 addTestClasses(value); 112 } else if (DEBUG_PARAM.equals(key)) { 113 mDebug = "true".equals(value) || "1".equals(value); 114 } else if (RUNNER_PARAM.equals(key)) { 115 mRunnerClassName = value; 116 } else { 117 mParams.putString(key, value); 118 } 119 } else { 120 return ARG_FAIL_INCOMPLETE_E; 121 } 122 } else if (args[i].equals("-c")) { 123 if (i + 1 < args.length) { 124 addTestClasses(args[++i]); 125 } else { 126 return ARG_FAIL_INCOMPLETE_C; 127 } 128 } else if (args[i].equals("--monkey")) { 129 mMonkey = true; 130 } else if (args[i].equals("-s")) { 131 mParams.putString(OUTPUT_FORMAT_KEY, OUTPUT_SIMPLE); 132 } else { 133 return ARG_FAIL_UNSUPPORTED; 134 } 135 } 136 return ARG_OK; 137 } 138 139 protected UiAutomatorTestRunner getRunner() { 140 if (mRunner != null) { 141 return mRunner; 142 } 143 144 if (mRunnerClassName == null) { 145 mRunner = new UiAutomatorTestRunner(); 146 return mRunner; 147 } 148 // use reflection to get the runner 149 Object o = null; 150 try { 151 Class<?> clazz = Class.forName(mRunnerClassName); 152 o = clazz.newInstance(); 153 } catch (ClassNotFoundException cnfe) { 154 System.err.println("Cannot find runner: " + mRunnerClassName); 155 System.exit(ARG_FAIL_RUNNER); 156 } catch (InstantiationException ie) { 157 System.err.println("Cannot instantiate runner: " + mRunnerClassName); 158 System.exit(ARG_FAIL_RUNNER); 159 } catch (IllegalAccessException iae) { 160 System.err.println("Constructor of runner " + mRunnerClassName + " is not accessibile"); 161 System.exit(ARG_FAIL_RUNNER); 162 } 163 try { 164 UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o; 165 mRunner = runner; 166 return runner; 167 } catch (ClassCastException cce) { 168 System.err.println("Specified runner is not subclass of " 169 + UiAutomatorTestRunner.class.getSimpleName()); 170 System.exit(ARG_FAIL_RUNNER); 171 } 172 // won't reach here 173 return null; 174 } 175 176 /** 177 * Add test classes from a potentially comma separated list 178 * @param classes 179 */ 180 private void addTestClasses(String classes) { 181 String[] classArray = classes.split(CLASS_SEPARATOR); 182 for (String clazz : classArray) { 183 mTestClasses.add(clazz); 184 } 185 } 186 187 /** 188 * Add test classes from jars passed on the command line. Use this if nothing was explicitly 189 * specified on the command line. 190 */ 191 private void addTestClassesFromJars() { 192 String jars = mParams.getString(JARS_PARAM); 193 if (jars == null) return; 194 195 String[] jarFileNames = jars.split(JARS_SEPARATOR); 196 for (String fileName : jarFileNames) { 197 fileName = fileName.trim(); 198 if (fileName.isEmpty()) continue; 199 try { 200 DexFile dexFile = new DexFile(fileName); 201 for(Enumeration<String> e = dexFile.entries(); e.hasMoreElements();) { 202 String className = e.nextElement(); 203 if (isTestClass(className)) { 204 mTestClasses.add(className); 205 } 206 } 207 dexFile.close(); 208 } catch (IOException e) { 209 Log.w(LOGTAG, String.format("Could not read %s: %s", fileName, e.getMessage())); 210 } 211 } 212 } 213 214 /** 215 * Tries to determine if a given class is a test class. A test class has to inherit from 216 * UiAutomator test case and it must be a top-level class. 217 * @param className 218 * @return 219 */ 220 private boolean isTestClass(String className) { 221 try { 222 Class<?> clazz = this.getClass().getClassLoader().loadClass(className); 223 if (clazz.getEnclosingClass() != null) return false; 224 return getRunner().getTestCaseFilter().accept(clazz); 225 } catch (ClassNotFoundException e) { 226 return false; 227 } 228 } 229 230 @Override 231 public String detailedOptions() { 232 return " runtest <class spec> [options]\n" 233 + " <class spec>: <JARS> < -c <CLASSES> | -e class <CLASSES> >\n" 234 + " <JARS>: a list of jar files containing test classes and dependencies. If\n" 235 + " the path is relative, it's assumed to be under /data/local/tmp. Use\n" 236 + " absolute path if the file is elsewhere. Multiple files can be\n" 237 + " specified, separated by space.\n" 238 + " <CLASSES>: a list of test class names to run, separated by comma. To\n" 239 + " a single method, use TestClass#testMethod format. The -e or -c option\n" 240 + " may be repeated. This option is not required and if not provided then\n" 241 + " all the tests in provided jars will be run automatically.\n" 242 + " options:\n" 243 + " --nohup: trap SIG_HUP, so test won't terminate even if parent process\n" 244 + " is terminated, e.g. USB is disconnected.\n" 245 + " -e debug [true|false]: wait for debugger to connect before starting.\n" 246 + " -e runner [CLASS]: use specified test runner class instead. If\n" 247 + " unspecified, framework default runner will be used.\n" 248 + " -e <NAME> <VALUE>: other name-value pairs to be passed to test classes.\n" 249 + " May be repeated.\n" 250 + " -e outputFormat simple | -s: enabled less verbose JUnit style output.\n"; 251 } 252 253 @Override 254 public String shortHelp() { 255 return "executes UI automation tests"; 256 } 257 258 } 259