package com.gorillalogic.monkeytalk.processor.command;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import com.gorillalogic.monkeytalk.Command;
import com.gorillalogic.monkeytalk.processor.PlaybackListener;
import com.gorillalogic.monkeytalk.processor.PlaybackResult;
import com.gorillalogic.monkeytalk.processor.PlaybackStatus;
import com.gorillalogic.monkeytalk.processor.Scope;
import com.gorillalogic.monkeytalk.processor.ScriptProcessor;
import com.gorillalogic.monkeytalk.sender.Response;
import com.gorillalogic.monkeytalk.sender.Response.ResponseStatus;
import com.gorillalogic.monkeytalk.utils.Base64;
import com.gorillalogic.monkeytalk.utils.ImageUtils;
public class VerifyImage extends BaseCommand {
ScriptProcessor scriptProcessor = null;
File rootDir;
String errorPrepend;
public VerifyImage(Command cmd, Scope scope, PlaybackListener listener,
ScriptProcessor scriptProcessor, File rootDir) {
super(cmd, scope, listener);
this.scriptProcessor = scriptProcessor;
this.rootDir = rootDir;
}
public PlaybackResult verifyImage() {
if (cmd.getArgs().size() == 0 || cmd.getArgs().get(0).length() == 0) {
return new PlaybackResult(PlaybackStatus.ERROR, "command '" + cmd.getCommand()
+ "' must have a file path as its first arg", scope);
}
boolean shouldCreateExpectedFile = false;
// validate file
String expectedFilePath = cmd.getArgs().get(0);
if (new File(expectedFilePath).isAbsolute()) {
return new PlaybackResult(
PlaybackStatus.ERROR,
"command '"
+ cmd.getCommand()
+ "' - expectedImageFile '"
+ expectedFilePath
+ "' is an absolute path reference"
+ "' - the expected image file path must be specified relative to the project directory",
scope);
}
File expectedFile = new File(rootDir, expectedFilePath);
if (!expectedFile.exists()) {
try {
File expectedFileParent = expectedFile.getParentFile();
if (!expectedFileParent.exists()) {
expectedFileParent.mkdirs();
}
expectedFile.createNewFile();
shouldCreateExpectedFile = true;
expectedFile.delete();
} catch (IOException e) {
return new PlaybackResult(PlaybackStatus.ERROR, "command '" + cmd.getCommand()
+ "' - expectedImageFile '" + expectedFilePath + "'"
+ " does not exist and cannot be created: " + e.getMessage(), scope);
}
} else {
if (!expectedFile.isFile()) {
return new PlaybackResult(PlaybackStatus.ERROR, "command '" + cmd.getCommand()
+ "' - expectedImageFile '" + expectedFilePath + "'"
+ " is not a regular file, perhaps a folder?", scope);
}
}
// optional tolerance
int tolerance = ImageUtils.DEFAULT_TOLERANCE;
if (cmd.getArgs().size() > 1) {
boolean badTolerance = false;
try {
tolerance = Integer.parseInt(cmd.getArgs().get(1));
} catch (NumberFormatException nfe) {
badTolerance = true;
}
if (!badTolerance) {
if (tolerance < ImageUtils.MIN_TOLERANCE || tolerance > ImageUtils.MAX_TOLERANCE) {
badTolerance = true;
}
}
if (badTolerance) {
return new PlaybackResult(PlaybackStatus.ERROR, "command '" + cmd.getCommand()
+ "' - tolerance '" + cmd.getArgs().get(1)
+ "' is invalid: must be an integer" + " between "
+ ImageUtils.MIN_TOLERANCE + " and " + ImageUtils.MAX_TOLERANCE, scope);
}
}
listener.onStart(scope);
PlaybackResult result = doVerifyImage(expectedFile, shouldCreateExpectedFile, tolerance);
ResponseStatus responseStatus = ResponseStatus.OK;
if (result.getStatus().equals(PlaybackStatus.ERROR)) {
responseStatus = ResponseStatus.ERROR;
} else if (result.getStatus().equals(PlaybackStatus.FAILURE)) {
responseStatus = ResponseStatus.FAILURE;
}
Response resp = new Response(responseStatus, result.getMessage(), result.getWarning(),
result.getImage());
// log("listener: class=" + listener.getClass().getName());
listener.onComplete(scope, resp);
return result;
}
public PlaybackResult doVerifyImage(File expectedFile, boolean shouldCreateExpectedFile,
int tolerance) {
// agent will take a screenshot and also return the
// rectangle of the target component in the message
Response resp = scriptProcessor.runCommand(cmd);
result = new PlaybackResult(resp, scope);
if (!result.getStatus().equals(PlaybackStatus.OK)) {
String errorMessage = "command '" + cmd.getCommand() + "'";
String resultMessage = result.getMessage();
if (resultMessage == null || resultMessage.length() == 0) {
errorMessage += " - no message from agent";
} else {
if (!resultMessage.contains(errorMessage)) {
errorMessage += " - " + resultMessage;
} else {
errorMessage = resultMessage;
}
}
result.setMessage(errorMessage);
return result;
}
String expectedFilePath = expectedFile.getPath();
byte[] image = getImageForVerify(result);
if (result.getStatus().equals(PlaybackStatus.ERROR)) {
return result;
}
if (shouldCreateExpectedFile) {
try {
FileUtils.writeByteArrayToFile(expectedFile, image);
} catch (IOException e) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand()
+ "' - error creating image file: " + e.getMessage());
}
String msg = "command '" + cmd.getCommand() + "' - file '" + expectedFilePath
+ "' was not found, creating it with the just-captured image";
result.setWarning(result.getWarning() == null ? msg : result.getWarning() + "; " + msg);
result.setMessage(msg);
} else {
byte[] expectedImage = null;
try {
expectedImage = FileUtils.readFileToByteArray(expectedFile);
} catch (IOException e) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand() + "' - error reading image file '"
+ expectedFilePath + "': " + e.getMessage());
return result;
}
if (expectedImage == null || expectedImage.length == 0) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand() + "' - expected image file '"
+ expectedFilePath + "' was empty or could not be read.");
return result;
}
// at last
try {
if (!compareImages(expectedImage, image, tolerance)) {
result.setStatus(PlaybackStatus.FAILURE);
result.setMessage("command '" + cmd.getCommand()
+ "' - expected and captured images do not match.");
} else {
// YAY!
result.setStatus(PlaybackStatus.OK);
result.setMessage("");
}
} catch (IOException e) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand()
+ "' - error comparing expected and actual images: " + e.getMessage());
}
}
return result;
}
private byte[] getImageForVerify(PlaybackResult result) {
String imageStr = result.getImage();
if (imageStr == null || imageStr.length() == 0) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand() + "' - no screenshot received");
return null;
}
byte[] image = null;
try {
image = Base64.decode(imageStr);
System.out.println("imageStr length=" + imageStr.length() + " chars, decodes to "
+ image.length + " bytes");
} catch (IOException e) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand() + "' - error decoding image: "
+ e.getMessage());
return null;
}
// get bounding rectangle if any, and clip image
String agentMessage = result.getMessage();
if (agentMessage != null && agentMessage.length() > 0) {
boolean shouldWarn = false;
// x, y, w, h
String[] dimensions = agentMessage.split(" ");
if (dimensions.length == 4) {
try {
int x = Integer.parseInt(dimensions[0]);
int y = Integer.parseInt(dimensions[1]);
int w = Integer.parseInt(dimensions[2]);
int h = Integer.parseInt(dimensions[3]);
if(x < 0 || y < 0 || w < 0 || h < 0) { //make sure dimensions are valid
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("\"" + cmd.getComponentType() + "\" with MonkeyID, \"" + cmd.getMonkeyId() + "\" has invalid dimensions!");
return null;
}
image = cropImage(image, x, y, w, h);
if (image == null) {
result.setStatus(PlaybackStatus.ERROR);
result.setMessage("command '" + cmd.getCommand()
+ "' - error cropping image");
return null;
}
} catch (NumberFormatException nfe) {
shouldWarn = true;
}
} else {
shouldWarn = true;
}
if (shouldWarn) {
String msg = "could not parse component rectangle from this string: " + dimensions;
result.setWarning(result.getWarning() == null ? msg : result.getWarning() + "; "
+ msg);
}
}
return image;
}
private byte[] cropImage(byte[] image, int x, int y, int w, int h) {
try {
BufferedImage uncroppedImage = ImageIO.read(new ByteArrayInputStream(image));
// log("uncropped: width=" + uncroppedImage.getWidth() + " height=" +
// uncroppedImage.getHeight());
if (uncroppedImage == null) {
log("invalid screenshot - image could not be created");
return null;
}
BufferedImage croppedImage = ImageUtils.cropImage(uncroppedImage, x, y, w, h);
// log("cropped: width=" + croppedImage.getWidth() + " height=" +
// croppedImage.getHeight());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(croppedImage, "png", baos);
image = baos.toByteArray();
// log("cropped image has length: " + image.length);
} catch (IOException e) {
log("error creating image from supplied bitmap: " + e.getMessage());
return null;
}
return image;
}
private boolean compareImages(byte[] expected, byte[] actual, int tolerance) throws IOException {
log("comparing expectedImage of length: " + expected.length
+ " with actualImage of length: " + actual.length);
BufferedImage expectedImage = ImageIO.read(new ByteArrayInputStream(expected));
BufferedImage actualImage = ImageIO.read(new ByteArrayInputStream(actual));
return ImageUtils.compare(expectedImage, actualImage, tolerance);
}
}