org.sikuli.basics.Debug Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of sikulixapi Show documentation
Show all versions of sikulixapi Show documentation
... for visual testing and automation
/*
* Copyright 2010-2014, Sikuli.org, sikulix.com
* Released under the MIT License.
*
* modified RaiMan 2013
*/
package org.sikuli.basics;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import org.sikuli.util.JythonHelper;
/**
* Debug is a utility class that wraps println statements and allows more or less command line
* output to be turned on.
For debug messages only ( Debug.log() ):
Use system
* property: sikuli.Debug to set the debug level (default = 1)
On the command line, use
* -Dsikuli.Debug=n to set it to level n
-Dsikuli.Debug will disable any debug messages
* (which is equivalent to using Settings.Debuglogs = false)
It prints if the level
* number is less than or equal to the currently set DEBUG_LEVEL.
For messages
* ActionLogs, InfoLogs see Settings
You might send all messages generated by this
* class to a file:
-Dsikuli.Logfile=pathname (no path given: SikuliLog.txt in working
* folder)
This can be restricted to Debug.user only (others go to System.out):
* -Dsikuli.LogfileUser=pathname (no path given: UserLog.txt in working folder)
*
* You might redirect info, action, error and debug messages to your own logger object
* Start with setLogger() and then define with setLoggerXyz() the redirection targets
*
* This solution is NOT threadsafe !!!
*/
public class Debug {
private static int DEBUG_LEVEL = 0;
private static boolean loggerRedirectSupported = true;
private long _beginTime = 0;
private String _message;
private String _title = null;
private static PrintStream printout = null;
private static PrintStream printoutuser = null;
private static final DateFormat df =
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);
public static String logfile;
private static Object privateLogger = null;
private static boolean privateLoggerPrefixAll = true;
private static Method privateLoggerUser = null;
private static String privateLoggerUserName = "";
private static String privateLoggerUserPrefix = "";
private static Method privateLoggerInfo = null;
private static String privateLoggerInfoName = "";
private static final String infoPrefix = "info";
private static String privateLoggerInfoPrefix = "[" + infoPrefix + "] ";
private static Method privateLoggerAction = null;
private static String privateLoggerActionName = "";
private static final String actionPrefix = "log";
private static String privateLoggerActionPrefix = "[" + actionPrefix + "] ";
private static Method privateLoggerError = null;
private static String privateLoggerErrorName = "";
private static final String errorPrefix = "error";
private static String privateLoggerErrorPrefix = "[" + errorPrefix + "] ";
private static Method privateLoggerDebug = null;
private static String privateLoggerDebugName = "";
private static final String debugPrefix = "debug";
private static String privateLoggerDebugPrefix = "";
private static boolean isJython;
private static boolean isJRuby;
private static Object scriptRunner = null;
private static boolean searchHighlight = false;
private static PrintStream redirectedOut = null, redirectedErr = null;
static {
String debug = System.getProperty("sikuli.Debug");
if (debug != null && "".equals(debug)) {
DEBUG_LEVEL = 0;
Settings.DebugLogs = false;
} else {
try {
DEBUG_LEVEL = Integer.parseInt(debug);
if (DEBUG_LEVEL > 0) {
Settings.DebugLogs = true;
} else {
Settings.DebugLogs = false;
}
} catch (NumberFormatException numberFormatException) {
}
}
setLogFile(null);
setUserLogFile(null);
}
public static void init() {
if (DEBUG_LEVEL > 0) {
logx(DEBUG_LEVEL, "Debug.init: from sikuli.Debug: on: %d", DEBUG_LEVEL);
}
}
public static void highlightOn() {
searchHighlight = true;
Settings.Highlight = true;
}
public static void highlightOff() {
searchHighlight = false;
Settings.Highlight = false;
}
public static boolean shouldHighlight() {
return searchHighlight;
}
/**
* A logger object that is intended, to get Sikuli's log messages per redirection
* @param logger the logger object
*/
public static void setLogger(Object logger) {
if (!doSetLogger(logger)) return;
privateLoggerPrefixAll = true;
logx(3, "Debug: setLogger %s", logger);
}
/**
* same as setLogger(), but the Sikuli prefixes are omitted in all redirected messages
* @param logger the logger object
*/
public static void setLoggerNoPrefix(Object logger) {
if (!doSetLogger(logger)) return;
privateLoggerPrefixAll = false;
}
private static boolean doSetLogger(Object logger) {
String className = logger.getClass().getName();
isJython = className.contains("org.python");
isJRuby = className.contains("org.jruby");
if ( isJRuby ) {
logx(3, "Debug: setLogger: given instance's class: %s", className);
error("setLogger: not yet supported in JRuby script");
loggerRedirectSupported=false;
return false;
}
privateLogger = logger;
return true;
}
/**
* sets the redirection for all message types user, info, action, error and debug
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the message text
* @param mAll name of the method where the message should be sent
* @return true if the method is available false otherwise */
public static boolean setLoggerAll(String mAll) {
if (!loggerRedirectSupported) {
logx(3, "Debug: setLoggerAll: logger redirect not supported");
return false;
}
if (privateLogger != null) {
logx(3, "Debug.setLoggerAll: %s", mAll);
boolean success = true;
success &= setLoggerUser(mAll);
success &= setLoggerInfo(mAll);
success &= setLoggerAction(mAll);
success &= setLoggerError(mAll);
success &= setLoggerDebug(mAll);
return success;
}
return false;
}
private static boolean doSetLoggerCallback(String mName, CallbackType type) {
if (privateLogger == null) {
error("Debug: setLogger: no logger specified yet");
return false;
}
if (!loggerRedirectSupported) {
logx(3, "Debug: setLogger: %s (%s) logger redirect not supported", mName, type);
}
if (isJython) {
Object[] args = new Object[]{privateLogger, mName, type.toString()};
if (!JythonHelper.get().checkCallback(args)) {
logx(3, "Debug: setLogger: Jython: checkCallback returned: %s", args[0]);
return false;
}
}
try {
if (type == CallbackType.INFO) {
if ( !isJython && !isJRuby ) {
privateLoggerInfo = privateLogger.getClass().getMethod(mName, new Class[]{String.class});
}
privateLoggerInfoName = mName;
return true;
} else if (type == CallbackType.ACTION) {
if ( !isJython && !isJRuby ) {
privateLoggerAction = privateLogger.getClass().getMethod(mName, new Class[]{String.class});
}
privateLoggerActionName = mName;
return true;
} else if (type == CallbackType.ERROR) {
if ( !isJython && !isJRuby ) {
privateLoggerError = privateLogger.getClass().getMethod(mName, new Class[]{String.class});
}
privateLoggerErrorName = mName;
return true;
} else if (type == CallbackType.DEBUG) {
if ( !isJython && !isJRuby ) {
privateLoggerDebug = privateLogger.getClass().getMethod(mName, new Class[]{String.class});
}
privateLoggerDebugName = mName;
return true;
} else if (type == CallbackType.USER) {
if ( !isJython && !isJRuby ) {
privateLoggerUser = privateLogger.getClass().getMethod(mName, new Class[]{String.class});
}
privateLoggerUserName = mName;
return true;
} else {
return false;
}
} catch (Exception e) {
error("Debug: setLoggerInfo: redirecting to %s failed: \n%s", mName, e.getMessage());
}
return false;
}
/**
* specify the target method for redirection of Sikuli's user log messages [user]
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the info message
* @param mUser name of the method where the message should be sent
*
reset to default logging by either null or empty string
* @return true if the method is available false otherwise
*/
public static boolean setLoggerUser(String mUser) {
if (mUser == null || mUser.isEmpty()) {
privateLoggerUserName = "";
return true;
}
return doSetLoggerCallback(mUser, CallbackType.USER);
}
/**
* specify the target method for redirection of Sikuli's info messages [info]
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the info message
* @param mInfo name of the method where the message should be sent
*
reset to default logging by either null or empty string
* @return true if the method is available false otherwise
*/
public static boolean setLoggerInfo(String mInfo) {
if (mInfo == null || mInfo.isEmpty()) {
privateLoggerInfoName = "";
return true;
}
return doSetLoggerCallback(mInfo, CallbackType.INFO);
}
/**
* specify the target method for redirection of Sikuli's action messages [log]
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the info message
* @param mAction name of the method where the message should be sent
*
reset to default logging by either null or empty string
* @return true if the method is available false otherwise
*/
public static boolean setLoggerAction(String mAction) {
if (mAction == null || mAction.isEmpty()) {
privateLoggerActionName = "";
return true;
}
return doSetLoggerCallback(mAction, CallbackType.ACTION);
}
/**
* specify the target method for redirection of Sikuli's error messages [error]
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the info message
* @param mError name of the method where the message should be sent
*
reset to default logging by either null or empty string
* @return true if the method is available false otherwise
*/
public static boolean setLoggerError(String mError) {
if (mError == null || mError.isEmpty()) {
privateLoggerErrorName = "";
return true;
}
return doSetLoggerCallback(mError, CallbackType.ERROR);
}
/**
* specify the target method for redirection of Sikuli's debug messages [debug]
* must be the name of an instance method of the previously defined logger and
* must accept exactly one string parameter, that contains the info message
* @param mDebug name of the method where the message should be sent
*
reset to default logging by either null or empty string
* @return true if the method is available false otherwise
*/
public static boolean setLoggerDebug(String mDebug) {
if (mDebug == null || mDebug.isEmpty()) {
privateLoggerDebugName = "";
return true;
}
return doSetLoggerCallback(mDebug, CallbackType.DEBUG);
}
public static void saveRedirected(PrintStream rdo, PrintStream rde) {
redirectedOut = rdo;
redirectedErr = rde;
}
public static void out(String msg) {
if (redirectedOut != null && DEBUG_LEVEL > 2) {
redirectedOut.println(msg);
}
}
/**
* specify, where the logs should be written:
* null - use from property sikuli.Logfile
* empty - use SikuliLog.txt in working folder
* not empty - use given filename
* @param fileName null, empty or absolute filename
* @return success
*/
public static boolean setLogFile(String fileName) {
if (fileName == null) {
fileName = System.getProperty("sikuli.Logfile");
}
if (fileName != null) {
if ("".equals(fileName)) {
if (Settings.isMacApp) {
fileName = "SikulixLog.txt";
} else {
fileName = FileManager.slashify(System.getProperty("user.dir"), true) + "SikulixLog.txt";
}
}
try {
logfile = fileName;
if (printout != null) {
printout.close();
}
printout = new PrintStream(fileName);
log(3, "Debug: setLogFile: " + fileName);
return true;
} catch (Exception ex) {
System.out.printf("[Error] Logfile %s not accessible - check given path", fileName);
System.out.println();
return false;
}
}
return false;
}
/**
* does Sikuli log go to a file?
* @return true if yes, false otherwise
*/
public static boolean isLogToFile() {
return (printout != null);
}
/**
* specify, where the user logs (Debug.user) should be written:
* null - use from property sikuli.LogfileUser
* empty - use UserLog.txt in working folder
* not empty - use given filename
* @param fileName null, empty or absolute filename
* @return success
*/
public static boolean setUserLogFile(String fileName) {
if (fileName == null) {
fileName = System.getProperty("sikuli.LogfileUser");
}
if (fileName != null) {
if ("".equals(fileName)) {
if (Settings.isMacApp) {
fileName = "UserLog.txt";
} else {
fileName = FileManager.slashify(System.getProperty("user.dir"), true) + "UserLog.txt";
}
}
try {
if (printoutuser != null) {
printoutuser.close();
}
printoutuser = new PrintStream(fileName);
log(3, "Debug: setLogFile: " + fileName);
return true;
} catch (FileNotFoundException ex) {
System.out.printf("[Error] User logfile %s not accessible - check given path", fileName);
System.out.println();
return false;
}
}
return false;
}
/**
* does user log go to a file?
* @return true if yes, false otherwise
*/
public static boolean isUserLogToFile() {
return (printoutuser != null);
}
/**
*
* @return current debug level
*/
public static int getDebugLevel() {
return DEBUG_LEVEL;
}
/**
* set debug level to default level
*
* @return default level
*/
public static int setDebugLevel() {
setDebugLevel(0);
return DEBUG_LEVEL;
}
/**
* set debug level to given value
*
* @param level value
*/
public static void setDebugLevel(int level) {
DEBUG_LEVEL = level;
if (DEBUG_LEVEL > 0) {
Settings.DebugLogs = true;
} else {
Settings.DebugLogs = false;
}
}
public static void on(int level) {
setDebugLevel(level);
}
public static void on(String level) {
setDebugLevel(level);
}
public static boolean is(int level) {
return DEBUG_LEVEL >= level;
}
public static int is() {
return DEBUG_LEVEL;
}
public static void off() {
setDebugLevel(0);
}
/**
* set debug level to given number value as string (ignored if invalid)
*
* @param level valid number string
*/
public static void setDebugLevel(String level) {
try {
DEBUG_LEVEL = Integer.parseInt(level);
if (DEBUG_LEVEL > 0) {
Settings.DebugLogs = true;
} else {
Settings.DebugLogs = false;
}
} catch (NumberFormatException e) {
}
}
private static boolean doRedirect(CallbackType type, String pre, String message, Object... args) {
boolean success = false;
String error = "";
if (privateLogger != null) {
String prefix = "", pln = "";
Method plf = null;
if (type == CallbackType.INFO && !privateLoggerInfoName.isEmpty()) {
prefix = privateLoggerPrefixAll ? privateLoggerInfoPrefix : "";
plf = privateLoggerInfo;
pln = privateLoggerInfoName;
} else if (type == CallbackType.ACTION && !privateLoggerActionName.isEmpty()) {
prefix = privateLoggerPrefixAll ? privateLoggerActionPrefix : "";
plf = privateLoggerAction;
pln = privateLoggerActionName;
} else if (type == CallbackType.ERROR && !privateLoggerErrorName.isEmpty()) {
prefix = privateLoggerPrefixAll ? privateLoggerErrorPrefix : "";
plf = privateLoggerError;
pln = privateLoggerErrorName;
} else if (type == CallbackType.DEBUG && !privateLoggerDebugName.isEmpty()) {
prefix = privateLoggerPrefixAll ?
(privateLoggerDebugPrefix.isEmpty() ? pre : privateLoggerDebugPrefix) : "";
plf = privateLoggerDebug;
pln = privateLoggerDebugName;
} else if (type == CallbackType.USER && !privateLoggerUserName.isEmpty()) {
prefix = privateLoggerPrefixAll ?
(privateLoggerUserPrefix.isEmpty() ? pre : privateLoggerUserPrefix) : "";
plf = privateLoggerUser;
pln = privateLoggerUserName;
}
if (!pln.isEmpty()) {
String msg = null;
if (args == null) {
msg = prefix + message;
} else {
msg = String.format(prefix + message, args);
}
if (isJython) {
success = JythonHelper.get().runLoggerCallback(new Object[]{privateLogger, pln, msg});
} else if (isJRuby) {
success = false;
} else {
try {
plf.invoke(privateLogger,
new Object[]{msg});
return true;
} catch (Exception e) {
error = ": " + e.getMessage();
success = false;
}
}
if (!success) {
Debug.error("calling (%s) logger.%s failed - resetting to default%s", type, pln, error);
if (type == CallbackType.INFO) {
privateLoggerInfoName = "";
} else if (type == CallbackType.ACTION) {
privateLoggerActionName = "";
} else if (type == CallbackType.ERROR) {
privateLoggerErrorName = "";
} else if (type == CallbackType.DEBUG) {
privateLoggerDebugName = "";
} else if (type == CallbackType.USER) {
privateLoggerUserName = "";
}
}
}
}
return success;
}
/**
* Sikuli messages from actions like click, ...
switch on/off: Settings.ActionLogs
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void action(String message, Object... args) {
if (Settings.ActionLogs) {
if (doRedirect(CallbackType.ACTION, "", message, args)) {
return;
}
if (is(3)) {
logx(3, message, args);
} else {
log(-1, actionPrefix, message, args);
}
}
}
/**
* use Debug.action() instead
* @param message String or format string (String.format)
* @param args to use with format string
* @deprecated
*/
@Deprecated
public static void history(String message, Object... args) {
action(message, args);
}
/**
* informative Sikuli messages
switch on/off: Settings.InfoLogs
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void info(String message, Object... args) {
if (Settings.InfoLogs) {
if (doRedirect(CallbackType.INFO, "", message, args)) {
return;
}
log(-1, infoPrefix, message, args);
}
if (is(3)) {
logx(3, message, args);
}
}
/**
* Sikuli error messages
switch on/off: always on
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void error(String message, Object... args) {
if (doRedirect(CallbackType.ERROR, "", message, args)) {
return;
}
log(-1, errorPrefix, message, args);
}
/**
* Sikuli messages to use in tests
switch on/off: always on
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void test(String message, Object... args) {
if (message.contains("#returned#")) {
message = message.replace("#returned#", "returned: " +
((Boolean) args[0] ? "true" : "false"));
args = Arrays.copyOfRange(args, 1, args.length);
}
log(-1, "test", message, args);
}
/**
* Sikuli debug messages with default level
switch on/off: Settings.DebugLogs (off) and/or
* -Dsikuli.Debug
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void log(String message, Object... args) {
log(0, message, args);
}
/**
* messages given by the user
switch on/off: Settings.UserLogs
depending on
* Settings.UserLogTime, the prefix contains a timestamp
the user prefix (default "user")
* can be set: Settings,UserLogPrefix
*
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void user(String message, Object... args) {
if (Settings.UserLogs) {
if (Settings.UserLogTime) {
//TODO replace the hack -99 to filter user logs
log(-99, String.format("%s (%s)",
Settings.UserLogPrefix, df.format(new Date())), message, args);
} else {
log(-99, String.format("%s", Settings.UserLogPrefix), message, args);
}
}
}
/**
* Sikuli debug messages with level
switch on/off: Settings.DebugLogs (off) and/or
* -Dsikuli.Debug
*
* @param level value
* @param message String or format string (String.format)
* @param args to use with format string
*/
public static void log(int level, String message, Object... args) {
if (Settings.DebugLogs) {
log(level, debugPrefix, message, args);
}
}
/**
* INTERNAL USE: special debug messages
* @param level value
* @param message text or format string
* @param args for use with format string
*/
public static String logx(int level, String message, Object... args) {
String sout = "";
if (level == -1 || level == -100) {
sout = log(level, errorPrefix, message, args);
} else if (level == -2) {
sout = log(level, actionPrefix, message, args);
} else if (level == -3) {
sout = log(level, "", message, args);
} else {
sout = log(level, debugPrefix, message, args);
}
return sout;
}
public static String logp(String msg, Object... args) {
String out = String.format(msg, args);
System.out.println(out);
return out;
}
private static synchronized String log(int level, String prefix, String message, Object... args) {
//TODO replace the hack -99 to filter user logs
String sout = "";
String stime = "";
if (level <= DEBUG_LEVEL) {
if (Settings.LogTime && level != -99) {
stime = String.format(" (%s)", df.format(new Date()));
}
if (!prefix.isEmpty()) {
prefix = "[" + prefix + stime + "] ";
}
sout = String.format(message, args);
boolean isRedirected = false;
if (level > -99) {
isRedirected = doRedirect(CallbackType.DEBUG, prefix, sout, null);
} else if (level == -99) {
isRedirected = doRedirect(CallbackType.USER, prefix, sout, null);
}
if (!isRedirected) {
if (level == -99 && printoutuser != null) {
printoutuser.print(prefix + sout);
printoutuser.println();
} else if (printout != null) {
printout.print(prefix + sout);
printout.println();
} else {
System.out.print(prefix + sout);
System.out.println();
}
if (level == -1 || level == -100 || level > 2) {
out(prefix + sout);
}
}
}
return prefix + sout;
}
/**
* Sikuli profiling messages
switch on/off: Settings.ProfileLogs, default off
*
* @param message String or format string
* @param args to use with format string
*/
public static void profile(String message, Object... args) {
if (Settings.ProfileLogs) {
log(-1, "profile", message, args);
}
}
/**
* profile convenience: entering a method
* @param message String or format string
* @param args to use with format string
*/
public static void enter(String message, Object... args) {
profile("entering: " + message, args);
}
/**
* profile convenience: exiting a method
* @param message String or format string
* @param args to use with format string
*/
public static void exit(String message, Object... args) {
profile("exiting: " + message, args);
}
/**
* start timer
*
log output depends on Settings.ProfileLogs
* @return timer
*/
public static Debug startTimer() {
return startTimer("");
}
/**
* start timer with a message
*
log output depends on Settings.ProfileLogs
* @param message String or format string
* @param args to use with format string
* @return timer
*/
public static Debug startTimer(String message, Object... args) {
Debug timer = new Debug();
timer.startTiming(message, args);
return timer;
}
/**
* stop timer and print timer message
*
log output depends on Settings.ProfileLogs
*
* @return the time in msec
*/
public long end() {
if (_title == null) {
return endTiming(_message, false, new Object[0]);
} else {
return endTiming(_title, false, new Object[0]);
}
}
/**
* lap timer and print message with timer message
*
log output depends on Settings.ProfileLogs
*
* @param message String or format string
* @return the time in msec
*/
public long lap(String message) {
if (_title == null) {
return endTiming("(" + message + ") " + _message, true, new Object[0]);
} else {
return endTiming("(" + message + ") " + _title, true, new Object[0]);
}
}
private void startTiming(String message, Object... args) {
int pos;
if ((pos = message.indexOf("\t")) < 0) {
_title = null;
_message = message;
} else {
_title = message.substring(0, pos);
_message = message.replace("\t", " ");
}
if (!"".equals(_message)) {
profile("TStart: " + _message, args);
}
_beginTime = (new Date()).getTime();
}
private long endTiming(String message, boolean isLap, Object... args) {
if (_beginTime == 0) {
profile("TError: timer not started (%s)", message);
return -1;
}
long t = (new Date()).getTime();
long dt = t - _beginTime;
if (!isLap) {
_beginTime = 0;
}
if (!"".equals(message)) {
profile(String.format((isLap ? "TLap:" : "TEnd") +
" (%.3f sec): ", (float) dt / 1000) + message, args);
}
return dt;
}
private static enum CallbackType {
INFO, ACTION, ERROR, DEBUG, USER;
}
}