diff --git a/testrunner/test_defs.xml b/testrunner/test_defs.xml index 6f073bbb1..55ddfdfa7 100644 --- a/testrunner/test_defs.xml +++ b/testrunner/test_defs.xml @@ -291,6 +291,13 @@ See test_defs.xsd for more information. coverage_target="framework" cts="true" /> + + -yyyyMMdd-HH:mm:ss.xml, into the + * directory out/-yyyyMMdd-HH:mm:ss with the contents: + * + * + * + * script_name="filename" + * monkeyRunnerVersion="0.2" + * + * + * + * ... + * + * + * dateTime="20090921-17:08:43" + * + * + * + * ... + * + * dateTime="20090921-17:09:44" + * + * + * + * ... + * + * + * And then zip it up with all the screenshots in the file: -yyyyMMdd-HH:mm:ss.zip. + */ + +public class MonkeyRecorder { + + // xml file to store output results in + private static String mXmlFilename; + private static FileWriter mXmlFile; + private static XMLWriter mXmlWriter; + + // unique subdirectory to put results in (screenshots and xml file) + private static String mDirname; + private static List mScreenShotNames = new ArrayList(); + + // where we store all the results for all the script runs + private static final String ROOT_DIR = "out"; + + // for getting the date and time in now() + private static final SimpleDateFormat SIMPLE_DATE_TIME_FORMAT = new SimpleDateFormat("yyyyMMdd-HH:mm:ss"); + + /** + * Create a new MonkeyRecorder that records commands and zips up screenshots for submittal + * + * @param scriptName filepath of the monkey script we are running + */ + public MonkeyRecorder(String scriptName) throws IOException { + // Create directory structure to store xml file, images and zips + File scriptFile = new File(scriptName); + scriptName = scriptFile.getName(); // Get rid of path + mDirname = ROOT_DIR + "/" + stripType(scriptName) + "-" + now(); + new File(mDirname).mkdirs(); + + // Initialize xml file + mXmlFilename = stampFilename(stripType(scriptName) + ".xml"); + initXmlFile(scriptName); + } + + // Get the current date and time in a simple string format (used for timestamping filenames) + private static String now() { + return SIMPLE_DATE_TIME_FORMAT.format(Calendar.getInstance().getTime()); + } + + /** + * Initialize the xml file writer + * + * @param scriptName filename (not path) of the monkey script, stored as attribute in the xml file + */ + private static void initXmlFile(String scriptName) throws IOException { + mXmlFile = new FileWriter(mDirname + "/" + mXmlFilename); + mXmlWriter = new XMLWriter(mXmlFile); + mXmlWriter.begin(); + mXmlWriter.comment("Monkey Script Results"); + mXmlWriter.start("script_run"); + mXmlWriter.addAttribute("script_name", scriptName); + } + + /** + * Add a comment to the xml file. + * + * @param comment comment to add to the xml file + */ + public static void addComment(String comment) throws IOException { + mXmlWriter.comment(comment); + } + + /** + * Begin writing a command xml element + */ + public static void startCommand() throws IOException { + mXmlWriter.start("command"); + mXmlWriter.addAttribute("dateTime", now()); + } + + /** + * Write a command name attribute in a command xml element. + * It's add as a sinlge script command could be multiple monkey commands. + * + * @param cmd command sent to the monkey + */ + public static void addInput(String cmd) throws IOException { + String name = "cmd"; + String value = cmd; + mXmlWriter.tag("input", name, value); + } + + /** + * Write a response xml element in a command. + * Attributes include the monkey result, datetime, and possibly screenshot filename + * + * @param result response of the monkey to the command + * @param filename filename of the screen shot (or other file to be included) + */ + public static void addResult(String result, String filename) throws IOException { + int num_args = 2; + String[] names = new String[3]; + String[] values = new String[3]; + names[0] = "result"; + values[0] = result; + names[1] = "dateTime"; + values[1] = now(); + if (filename.length() != 0) { + names[2] = "screenshot"; + values[2] = stampFilename(filename); + addScreenShot(filename); + num_args = 3; + } + mXmlWriter.tag("response", names, values, num_args); + } + + /** + * Add an attribut to an xml element. name="escaped_value" + * + * @param name name of the attribute + * @param value value of the attribute + */ + public static void addAttribute(String name, String value) throws IOException { + mXmlWriter.addAttribute(name, value); + } + + /** + * Add an xml device variable element. name="escaped_value" + * + * @param name name of the variable + * @param value value of the variable + */ + public static void addDeviceVar(String name, String value) throws IOException { + String[] names = {"name", "value"}; + String[] values = {name, value}; + mXmlWriter.tag("device_var", names, values, names.length); + } + + /** + * Move the screenshot to storage and remember you did it so it can be zipped up later. + * + * @param filename file name of the screenshot to be stored (Not path name) + */ + private static void addScreenShot(String filename) { + File file = new File(filename); + String screenShotName = stampFilename(filename); + file.renameTo(new File(mDirname, screenShotName)); + mScreenShotNames.add(screenShotName); + } + + /** + * Finish writing a command xml element + */ + public static void endCommand() throws IOException { + mXmlWriter.end(); + } + + /** + * Add datetime in front of filetype (the stuff after and including the last infamous '.') + * + * @param filename path of file to be stamped + */ + private static String stampFilename(String filename) { + // + int typeIndex = filename.lastIndexOf('.'); + if (typeIndex == -1) { + return filename + "-" + now(); + } + return filename.substring(0, typeIndex) + "-" + now() + filename.substring(typeIndex); + } + + /** + * Strip out the file type (the stuff after and including the last infamous '.') + * + * @param filename path of file to be stripped of type information + */ + private static String stripType(String filename) { + // + int typeIndex = filename.lastIndexOf('.'); + if (typeIndex == -1) + return filename; + return filename.substring(0, typeIndex); + } + + /** + * Add a signature element + * + * @param filename path of file to be signatured + */ + private static void addMD5Signature(String filename) throws IOException { + String signature = ""; + // find signature... MD5 sig = new MD5(filename); signature = sig.toString(); + String[] names = new String[] { "type", "filename", "signature" }; + String[] values = new String[] { "md5", filename, signature }; + mXmlWriter.tag("Signature", names, values, values.length); + } + + + /** + * Close the monkeyRecorder by closing the xml file and zipping it up with the screenshots. + * + * @param filename path of file to be stripped of type information + */ + public static void close() throws IOException { + // zip up xml file and screenshots into ROOT_DIR. + byte[] buf = new byte[1024]; + String zipFileName = mXmlFilename + ".zip"; + endCommand(); + mXmlFile.close(); + FileOutputStream zipFile = new FileOutputStream(ROOT_DIR + "/" + zipFileName); + ZipOutputStream out = new ZipOutputStream(zipFile); + + // add the xml file + addFileToZip(out, mDirname + "/" + mXmlFilename, buf); + + // Add the screenshots + for (String filename : mScreenShotNames) { + addFileToZip(out, mDirname + "/" + filename, buf); + } + out.close(); + } + + /** + * Helper function to zip up a file into an open zip archive. + * + * @param zip the stream of the zip archive + * @param filepath the filepath of the file to be added to the zip archive + * @param buf storage place to stage reads of file before zipping + */ + private static void addFileToZip(ZipOutputStream zip, String filepath, byte[] buf) throws IOException { + FileInputStream in = new FileInputStream(filepath); + zip.putNextEntry(new ZipEntry(filepath)); + int len; + while ((len = in.read(buf)) > 0) { + zip.write(buf, 0, len); + } + zip.closeEntry(); + in.close(); + } +} diff --git a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java index cbc881c6c..07a473917 100644 --- a/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java +++ b/tools/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java @@ -30,18 +30,21 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.imageio.ImageIO; /** * MonkeyRunner is a host side application to control a monkey instance on a - * device. MonkeyRunner provides some useful helper functions to control the - * device as well as various other methods to help script tests. + * device. MonkeyRunner provides some useful helper functions to control the + * device as well as various other methods to help script tests. */ public class MonkeyRunner { @@ -50,27 +53,46 @@ public class MonkeyRunner { static Socket monkeySocket = null; static IDevice monkeyDevice; - + static BufferedReader monkeyReader; static BufferedWriter monkeyWriter; + static String monkeyResponse; + + static MonkeyRecorder monkeyRecorder; static String scriptName = null; + + // Obtain a suitable logger. + private static Logger logger = Logger.getLogger("com.android.monkeyrunner"); // delay between key events final static int KEY_INPUT_DELAY = 1000; + + // version of monkey runner + final static String monkeyRunnerVersion = "0.31"; - public static void main(String[] args) { + // TODO: interface cmd; class xml tags; fix logger; test class/script + public static void main(String[] args) throws IOException { + + // haven't figure out how to get below INFO...bad parent. Pass -v INFO to turn on logging + logger.setLevel(Level.parse("WARNING")); processOptions(args); + logger.info("initAdb"); initAdbConnection(); + logger.info("openMonkeyConnection"); openMonkeyConnection(); + logger.info("start_script"); start_script(); + logger.info("ScriptRunner.run"); ScriptRunner.run(scriptName); + logger.info("end_script"); end_script(); + logger.info("closeMonkeyConnection"); closeMonkeyConnection(); } @@ -166,13 +188,15 @@ public class MonkeyRunner { try { InetAddress addr = InetAddress.getByName(monkeyServer); monkeySocket = new Socket(addr, monkeyPort); + monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream())); + monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream())); } catch (UnknownHostException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } } - + /** * Close tcp session with the monkey on the device * @@ -189,47 +213,59 @@ public class MonkeyRunner { } /** - * This is a house cleaning type routine to run before starting a script. Puts - * the device in a known state. + * This is a house cleaning routine to run before starting a script. Puts + * the device in a known state and starts recording interesting info. */ - public static void start_script() { - press("menu"); - press("menu"); - press("home"); + public static void start_script() throws IOException { + press("menu", false); + press("menu", false); + press("home", false); + + // Start recording the script output, might want md5 signature of file for completeness + monkeyRecorder = new MonkeyRecorder(scriptName); + + // Record what device and version of software we are running on + monkeyRecorder.addAttribute("monkeyRunnerVersion", monkeyRunnerVersion); + addDeviceVars(); + monkeyRecorder.addComment("Script commands"); } - public static void end_script() { - String command = "END"; - sendMonkeyEvent(command); + /** + * This is a house cleaning routine to run after finishing a script. + * Puts the monkey server in a known state and closes the recording. + */ + public static void end_script() throws IOException { + String command = "done"; + sendMonkeyEvent(command, false, false); + + // Stop the recording and zip up the results + monkeyRecorder.close(); } /** This is a method for scripts to launch an activity on the device * * @param name The name of the activity to launch */ - public static void launch_activity(String name) { - try { - System.out.println("Launching: " + name); - monkeyDevice.executeShellCommand("am start -a android.intent.action.MAIN -n " - + name, new NullOutputReceiver()); - } catch (Exception e) { - e.printStackTrace(); - } - } + public static void launch_activity(String name) throws IOException { + System.out.println("Launching: " + name); + recordCommand("Launching: " + name); + monkeyDevice.executeShellCommand("am start -a android.intent.action.MAIN -n " + + name, new NullOutputReceiver()); + // void return, so no response given, just close the command element in the xml file. + monkeyRecorder.endCommand(); + } /** * Grabs the current state of the screen stores it as a png * * @param tag filename or tag descriptor of the screenshot */ - public static void grabscreen(String tag) { + public static void grabscreen(String tag) throws IOException { tag += ".png"; try { Thread.sleep(1000); getDeviceImage(monkeyDevice, tag, false); - } catch (IOException e) { - e.printStackTrace(); } catch (InterruptedException e) { } } @@ -239,9 +275,11 @@ public class MonkeyRunner { * * @param msec msecs to sleep for */ - public static void sleep(int msec) { + public static void sleep(int msec) throws IOException { try { + recordCommand("sleep: " + msec); Thread.sleep(msec); + recordResponse("OK"); } catch (InterruptedException e) { e.printStackTrace(); } @@ -253,12 +291,10 @@ public class MonkeyRunner { * @param x x-coordinate * @param y y-coordinate */ - public static boolean tap(int x, int y) { - String command = "touch down " + x + " " + y + "\r\n" + - "touch up " + x + " " + y + "\r\n"; - - System.out.println("Tapping: " + x + ", " + y); - return sendMonkeyEvent(command); + public static boolean tap(int x, int y) throws IOException { + String command = "tap " + x + " " + y; + boolean result = sendMonkeyEvent(command); + return result; } /** @@ -266,25 +302,33 @@ public class MonkeyRunner { * * @param key key to press */ - public static boolean press(String key) { - String command = "key down " + key + "\r\n" + - "key up " + key + "\r\n"; + public static boolean press(String key) throws IOException { + return press(key, true); + } - System.out.println("Pressing: " + key); - return sendMonkeyEvent(command); + /** + * Press function for scripts to call on a particular button or key + * + * @param key key to press + * @param print whether to send output to user + */ + private static boolean press(String key, boolean print) throws IOException { + String command = "press " + key; + boolean result = sendMonkeyEvent(command, print, true); + return result; } /** * dpad down function */ - public static boolean down() { + public static boolean down() throws IOException { return press("dpad_down"); } /** * dpad up function */ - public static boolean up() { + public static boolean up() throws IOException { return press("dpad_up"); } @@ -293,69 +337,173 @@ public class MonkeyRunner { * * @param text text to type */ - public static boolean type(String text) { - System.out.println("Typing: " + text); - for (int i=0; i 2) { + monkeyRecorder.addDeviceVar(name, monkeyResponse.substring(3)); + } else { + // only got OK - good variable but no value + monkeyRecorder.addDeviceVar(name, "null"); + } + } else { + // error returned - couldn't get var value for name... include error return + monkeyRecorder.addDeviceVar(name, monkeyResponse); + } + } else { + // no monkeyResponse - bad variable with no value + monkeyRecorder.addDeviceVar(name, "null"); + } + } + } else { + // it's an error, can't find variable names... + monkeyRecorder.addAttribute("listvar", monkeyResponse); + } + } + /** * Process the command-line options * * @return Returns true if options were parsed with no apparent errors. */ - public static void processOptions(String[] args) { + private static void processOptions(String[] args) { // parse command line parameters. int index = 0; @@ -364,7 +512,7 @@ public class MonkeyRunner { if ("-s".equals(argument)) { if(index == args.length) { - printAndExit("Missing Server after -s", false); + printUsageAndQuit("Missing Server after -s"); } monkeyServer = args[index++]; @@ -372,18 +520,32 @@ public class MonkeyRunner { } else if ("-p".equals(argument)) { // quick check on the next argument. if (index == args.length) { - printAndExit("Missing Server IP after -p", false /* terminate */); + printUsageAndQuit("Missing Server port after -p"); } monkeyPort = Integer.parseInt(args[index++]); - } else { - // get the filepath of the script to run. - scriptName = argument; - // should not be any other device. - //if (index < args.length) { - // printAndExit("Too many arguments!", false /* terminate */); - //} + } else if ("-v".equals(argument)) { + // quick check on the next argument. + if (index == args.length) { + printUsageAndQuit("Missing Log Level after -v"); + } + + Level level = Level.parse(args[index++]); + logger.setLevel(level); + level = logger.getLevel(); + System.out.println("Log level set to: " + level + "(" + level.intValue() + ")."); + System.out.println("Warning: Log levels below INFO(800) not working currently... parent issues"); + + } else if (argument.startsWith("-")) { + // we have an unrecognized argument. + printUsageAndQuit("Unrecognized argument: " + argument + "."); + + monkeyPort = Integer.parseInt(args[index++]); + + } else { + // get the filepath of the script to run. This will be the last undashed argument. + scriptName = argument; } } while (index < args.length); } @@ -394,22 +556,29 @@ public class MonkeyRunner { private static void getDeviceImage(IDevice device, String filepath, boolean landscape) throws IOException { RawImage rawImage; + recordCommand("grabscreen"); + System.out.println("Grabbing Screeshot: " + filepath + "."); try { rawImage = device.getScreenshot(); } catch (IOException ioe) { + recordResponse("No frame buffer", ""); printAndExit("Unable to get frame buffer: " + ioe.getMessage(), true /* terminate */); return; } // device/adb not available? - if (rawImage == null) + if (rawImage == null) { + recordResponse("No image", ""); return; - + } + assert rawImage.bpp == 16; BufferedImage image; + + logger.info("Raw Image - height: " + rawImage.height + ", width: " + rawImage.width); if (landscape) { // convert raw data to an Image @@ -458,16 +627,20 @@ public class MonkeyRunner { } if (!ImageIO.write(image, "png", new File(filepath))) { + recordResponse("No png writer", ""); throw new IOException("Failed to find png writer"); } + recordResponse("OK", filepath); } - private static void printUsageAndQuit() { + private static void printUsageAndQuit(String message) { // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + System.out.println(message); System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE"); System.out.println(""); System.out.println(" -s MonkeyServer IP Address."); System.out.println(" -p MonkeyServer TCP Port."); + System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)"); System.out.println(""); System.out.println("");