/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.fonemonkey.server; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.view.View; import com.gorillalogic.fonemonkey.ActivityManager; import com.gorillalogic.fonemonkey.Log; import com.gorillalogic.fonemonkey.Recorder; import com.gorillalogic.fonemonkey.automators.AutomationManager; import com.gorillalogic.fonemonkey.automators.DeviceAutomator; import com.gorillalogic.fonemonkey.automators.IAutomator; import com.gorillalogic.fonemonkey.exceptions.FoneMonkeyErrorException; import com.gorillalogic.fonemonkey.exceptions.FoneMonkeyFailureException; import com.gorillalogic.fonemonkey.exceptions.FoneMonkeyScriptFailure; import com.gorillalogic.monkeytalk.BuildStamp; import com.gorillalogic.monkeytalk.Command; import com.gorillalogic.monkeytalk.automators.AutomatorConstants; import com.gorillalogic.monkeytalk.server.JsonServer; import com.gorillalogic.monkeytalk.server.ServerConfig; public class PlaybackServer extends JsonServer { private static final Pattern VERIFY_FAIL_PATTERN = Pattern.compile("but found \"(.*)\""); public PlaybackServer() throws IOException { super(ServerConfig.DEFAULT_PLAYBACK_PORT_ANDROID); Log.log("starting PlaybackServer on port " + this.getPort()); } @Override public Response serve(String uri, String method, Map<String, String> headers, JSONObject json) { if ("GET".equals(method)) { return super.serve(uri, method, headers, json); } if (json == null) { return new Response(HttpStatus.INTERNAL_ERROR, "json body is null"); } String mtcommand = json.optString("mtcommand"); String result = "OK"; String message = ""; String warning = ""; if ("PING".equals(mtcommand)) { if (json.has("record")) { String record = json.optString("record"); boolean isRecording = "on".equalsIgnoreCase(record); Recorder.setRecording(isRecording); if (json.has("recordhost") && json.has("recordport")) { String recordHost = json.optString("recordhost"); int recordPort = json.optInt("recordport"); Recorder.setRecordServer(recordHost, recordPort); } message = "{" + "os:\"Android\"" + ",record:\"" + (isRecording ? "ON" : "OFF") + "\"" + ",mtversion:\"" + BuildStamp.VERSION + ((BuildStamp.BUILD_NUMBER != null && BuildStamp.BUILD_NUMBER.length() > 0) ? "_" : "") + BuildStamp.BUILD_NUMBER + " - " + BuildStamp.TIMESTAMP + "\"" + "}"; if (isRecording) { List<Command> list = Recorder.pollQueue(); if (list != null && list.size() > 0) { StringBuilder jlist = new StringBuilder("["); try { for (Command command : list) { if (jlist.length() > 1) { jlist.append(','); } String c = command.getCommandAsJSON(false).toString(); jlist.append(c); } jlist.append(']'); message = jlist.toString(); } catch (Exception e) { Log.log(e); } } } else { Recorder.clearQueue(); } } else { result = "ERROR"; message = "ping is missing the 'record' key"; } } else if ("PLAY".equals(mtcommand)) { Command cmd = new Command(json); // any PLAY turns off recording unless echo is on if (!"true".equals(cmd.getModifiers().get("echo"))) { // Log.log("Turning recording off for " + cmd); Recorder.setRecording(false); } else { // Log.log("Turning recording on for " + cmd); Recorder.setRecording(true); } // Find clipped views and throw error if (ActivityManager.getClippedViews() != null) { for (int i = 0; i < ActivityManager.getClippedViews().size(); i++) { IAutomator automator = AutomationManager.findAutomator(ActivityManager .getClippedViews().get(i)); // If view does not have a monkeyID ignore it if (automator.getMonkeyID().length() > 0) { result = "OK"; message = message + automator.getComponentType() + " with monkeyID " + automator.getMonkeyID() + " is being clipped. "; } } ActivityManager.clearClippedViews(); } Log.log("PLAYBACK - " + cmd.toString()); try { String val = play(cmd); if (val != null) { message = val; } } catch (FoneMonkeyFailureException ex) { result = "FAILURE"; message = (cmd.isScreenshotOnError() ? screenshotOnError(ex.getMessage()) : ex .getMessage()); } catch (FoneMonkeyErrorException ex) { result = "ERROR"; message = (cmd.isScreenshotOnError() ? screenshotOnError(ex.getMessage()) : ex .getMessage()); } // } else if ("RECORD".equals(mtcommand)) { // // IDE polling for recorded commands (from behind firewall) // List<Command> list = Recorder.pollQueue(); // StringBuilder jlist = new StringBuilder("["); // if (list != null && list.size() > 0) { // // try { // for (Command command : list) { // if (jlist.length() > 1) { // jlist.append(','); // } // String c = command.getCommandAsJSON(false).toString(); // jlist.append(c); // } // jlist.append(']'); // message = jlist.toString(); // } catch (Exception e) { // Log.log(e); // } // } } else if ("DUMPTREE".equals(mtcommand)) { message = AutomationManager.dumpViewTree(); } else if ("STOP".equals(mtcommand)) { // stop messages usually sent from newly started MT app to free up playback server port. stop(); message = "STOP"; Log.log("Playback server stopped"); } else { Log.log("UNKNOWN - " + json.toString()); result = "ERROR"; message = "unknown mtcommand=" + mtcommand; } JSONObject resp = new JSONObject(); try { resp.put("result", result); if (warning.length() > 0) { resp.put("warning", warning); } else { if (!"PING".equals(mtcommand)) { Log.log("sending " + mtcommand + " response: " + message); } resp.put( "message", (message.startsWith("{") ? new JSONObject(message) : message .startsWith("[") ? new JSONArray(message) : message)); } } catch (JSONException ex) { resp = new JSONObject(); } return new Response(HttpStatus.OK, resp); } private String play(Command cmd) throws FoneMonkeyErrorException, FoneMonkeyFailureException { // thinktime, before starting try { Thread.sleep(cmd.getThinktime()); } catch (InterruptedException ex) { // ignore this } long start = System.currentTimeMillis(); // play the command while (true) { boolean error = false; String msg = null; // handle wildcard monkey id if (isWildcardMonkeyIdVerify(cmd)) { List<String> fails = new ArrayList<String>(); List<IAutomator> automators = null; try { automators = AutomationManager.findAllWildcardMonkeyIdAutomators( cmd.getComponentType(), cmd.getMonkeyId()); } catch (IllegalArgumentException ex) { automators = null; error = true; msg = ex.getMessage(); Log.log("Error: " + msg); } catch (Exception ex) { automators = null; error = true; msg = ex.getClass().getName() + (ex.getMessage() != null ? " : " + ex.getMessage() : ""); Log.log("Error: " + msg, ex); } if (automators == null) { // ignore, msg already set } else if (automators.isEmpty()) { if (cmd.getArgs().size() == 0 && cmd.getAction().equalsIgnoreCase("verifynot")) { // success! - no automators & we are verifyNot return ""; } else { // Log.log("WildcardMonkeyIdVerify: no matches"); msg = "Unable to find " + printName(cmd); } } else { boolean verifyNotFailed = false; // Log.log("WildcardMonkeyIdVerify: # of matches = " + automators.size()); for (IAutomator automator : automators) { if (automator != null) { try { Object view = automator.getComponent(); if (view != null && view instanceof View && !((View) view).isShown()) { // found view, but not visible } else { Log.log("Play " + cmd.getComponentType() + "." + cmd.getAction() + " on " + automator.getComponentType() + "(" + automator.getMonkeyID() + ")"); msg = automator .play(cmd.getAction(), cmd.getArgs().toArray( new String[cmd.getArgs().size()])); // Log.log("WildcardMonkeyIdVerify: msg=" + msg); } } catch (FoneMonkeyScriptFailure ex) { // build a better error message Matcher m = VERIFY_FAIL_PATTERN.matcher(ex.getMessage()); if (m.find() && !fails.contains(m.group(1))) { fails.add(m.group(1)); } // Log.log("WildcardMonkeyIdVerify: failMsg=" + ex.getMessage()); msg = null; } catch (IllegalArgumentException ex) { error = true; msg = ex.getMessage(); Log.log("Error: " + msg); } catch (Exception ex) { msg = null; } } if (error) { //Its error. Not a failure case. } else if (cmd.getAction().toLowerCase().startsWith("verifynot")) { if (msg == null) { // fail! as least one verifyNot has failed verifyNotFailed = true; // Log.log("WildcardMonkeyIdVerify: verifyNot=true"); } } else { if (msg != null && msg.length() == 0) { // success! at least one verify succeeded return ""; } } } // all verifies failed if (error) { //Its error. Not a failure case. } else if (cmd.getArgs().size() == 0) { if (cmd.getAction().equalsIgnoreCase("verifynot")) { msg = "Found " + printName(cmd); } else { msg = "Unable to find " + printName(cmd); } } else { if (cmd.getAction().toLowerCase().startsWith("verifynot")) { if (verifyNotFailed) { msg = "Found \"" + cmd.getArgs().get(0) + "\" in " + printName(cmd); } else { // success! all verifyNots succeeded // Log.log("WildcardMonkeyIdVerify: all verifyNots succeeded"); return ""; } } else { msg = "Expected \"" + cmd.getArgs().get(0) + "\", but found " + fails; } } // Log.log("WildcardMonkeyIdVerify: failed - " + msg); } } else { // not wildcard monkeyId verify, so just find 1st match try { IAutomator automator = AutomationManager.find(cmd.getComponentType(), cmd.getMonkeyId(), true); if (automator == null) { // verify that the component does NOT exist (we must do this here) if (cmd.getAction().toLowerCase().startsWith("verifynot") && cmd.getArgs().size() == 0) { return ""; } else { msg = "Unable to find " + printName(cmd); } } else { Object view = automator.getComponent(); if (view != null && view instanceof View && !((View) view).isShown()) { msg = "Found " + printName(cmd) + ", but not visible"; } else { Log.log("Play " + cmd.getComponentType() + "." + cmd.getAction() + " on " + automator.getComponentType() + "(" + automator.getMonkeyID() + ")"); return automator.play(cmd.getAction(), cmd.getArgs().toArray(new String[cmd.getArgs().size()])); } } } catch (FoneMonkeyScriptFailure ex) { msg = ex.getMessage(); } catch (IllegalArgumentException ex) { error = true; msg = ex.getMessage(); Log.log("Error: " + msg); } catch (Exception ex) { error = true; msg = ex.getClass().getName() + (ex.getMessage() != null ? " : " + ex.getMessage() : ""); Log.log("Error: " + msg, ex); } } // timeout? if (System.currentTimeMillis() - start > cmd.getTimeout()) { if (error) { throw new FoneMonkeyErrorException(msg); } // not an error, so we must have a failure here throw new FoneMonkeyFailureException(msg); } // sleep, then loop again try { Thread.sleep(cmd.getRetryDelay()); } catch (InterruptedException ex) { // ignore this } } } private String printName(Command cmd) { return cmd.getComponentType() + "(" + cmd.getMonkeyId() + ")"; } private String screenshotOnError(String msg) { if (msg == null) { msg = "no message"; } Log.log("SCREENSHOT - " + msg + " - taking screenshot..."); DeviceAutomator device = (DeviceAutomator) AutomationManager.findAutomatorByType("Device"); if (device != null) { try { String screenshot = device.play(AutomatorConstants.ACTION_SCREENSHOT); if (screenshot != null && screenshot.startsWith("{screenshot")) { Log.log("SCREENSHOT - done!"); return "{message:\"" + msg.replaceAll("\"", "'") + "\"," + screenshot.substring(1); } } catch (Exception ex) { String exMsg = ex.getMessage(); if (exMsg != null) { exMsg = exMsg.replaceAll("\"", "'"); } else { exMsg = ex.getClass().getName(); } return msg + " -- " + exMsg; } } return msg; } /** Helper to check if the given command is a Wildcard MonkeyId Verify command. */ private boolean isWildcardMonkeyIdVerify(Command cmd) { return cmd != null && cmd.getAction() != null && cmd.getMonkeyId() != null && cmd.getAction().toLowerCase().startsWith("verify") && !cmd.getAction().equalsIgnoreCase("verifyimage") && (cmd.getMonkeyId().contains("*") || cmd.getMonkeyId().contains("?")) && !cmd.getMonkeyId().toLowerCase().startsWith("xpath=") && !cmd.getComponentType().equalsIgnoreCase("device"); } }