diff --git a/AUTHORS.txt b/AUTHORS.txt index 582711d..9a5e4bc 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -8,4 +8,4 @@ Tatu Lahtela Find All With Pseudo Class and Find Class keywords Sakari Hoisko Dockerized linux env with X Juho Lehtonen Optimized docker environment. Juho Saarinen Package improvements, initial monocle support, screenshot bug fix - +Turo Soisenniemi Initial java agent support diff --git a/README.md b/README.md index aea0021..6b26cec 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ Executing _test.sh_ runs the acceptance suite twice: first using JavaFXLibrary a If you want the suite to run only once, you can define which type of library to use by including **local** or **remote** as an argument. For example command `test.sh remote` will execute the suite only in remote mode. ## Experimental: Headless support -Library supports headless operation utilizing [Monocle](https://wiki.openjdk.java.net/display/OpenJFX/Monocle). The support for this is still at experimental level. +Library supports headless operation utilizing [Monocle](https://wiki.openjdk.java.net/display/OpenJFX/Monocle). The support for this is still at experimental level. ### Main issues with headless function * Scrolling doesn't work same way as with screen @@ -97,4 +97,7 @@ Remote: ``` *** Settings *** Library Remote http://127.0.0.1:8270 ${True} WITH NAME JavaFXLibrary -``` \ No newline at end of file +``` + +## Experimental: Java agent support +Library can be used as java agent. Launch application with `-javaagent:/path/to/javafxlibrary-.jar`. Default port is 8270 and can be changed with adding `=` to java agent command. Only remote library is supported. Using launch keyword is still required but instead of starting new application keyword initializes Stage for library. diff --git a/pom.xml b/pom.xml index 1aeb927..cc67a5d 100644 --- a/pom.xml +++ b/pom.xml @@ -221,6 +221,9 @@ true JavaFXLibrary + + JavaFXLibrary + diff --git a/src/main/java/JavaFXLibrary.java b/src/main/java/JavaFXLibrary.java index 63523a9..8e9dd06 100644 --- a/src/main/java/JavaFXLibrary.java +++ b/src/main/java/JavaFXLibrary.java @@ -56,12 +56,12 @@ public class JavaFXLibrary extends AnnotationLibrary { add("javafxlibrary/keywords/Keywords/*.class"); add("javafxlibrary/keywords/*.class"); add("javafxlibrary/tests/*.class"); - }}; + }}; public JavaFXLibrary() { - this(false); + this(false); } - + public JavaFXLibrary(boolean headless) { super(includePatterns); if (headless) { @@ -71,9 +71,9 @@ public JavaFXLibrary(boolean headless) { System.setProperty("prism.text", "t2k"); TestFxAdapter.isHeadless = true; } else { - //v4.0.15-alpha sets default robot as glass, which breaks rolling - //Forcing usage of awt robot as previous versions - System.setProperty("testfx.robot", "awt"); + // v4.0.15-alpha sets default robot as glass, which breaks rolling + // Forcing usage of awt robot as previous versions + System.setProperty("testfx.robot", "awt"); } } @@ -108,7 +108,6 @@ public Object runKeyword(String keywordName, List args, Map kwargs) { finalKwargs = kwargs; } - AtomicReference retval = new AtomicReference<>(); AtomicReference retExcep = new AtomicReference<>(); @@ -121,12 +120,12 @@ public Object runKeyword(String keywordName, List args, Map kwargs) { retval.set(super.runKeyword(keywordName, finalArgs, finalKwargs)); return true; - } catch (JavaFXLibraryTimeoutException jfxte){ + } catch (JavaFXLibraryTimeoutException jfxte) { // timeout already expired, catch exception and jump out retExcep.set(jfxte); throw jfxte; - } catch (RuntimeException e){ + } catch (RuntimeException e) { // catch exception and continue trying retExcep.set(e); return false; @@ -165,10 +164,11 @@ public Object runKeyword(String keywordName, List args) { List finalArgs; // JavalibCore changes arguments of Call Method keywords to Strings after this check, so they need to handle their own objectMapping - if (!(keywordName.equals("callObjectMethod") || keywordName.equals("callObjectMethodInFxApplicationThread"))) + if (!(keywordName.equals("callObjectMethod") || keywordName.equals("callObjectMethodInFxApplicationThread"))) { finalArgs = HelperFunctions.useMappedObjects(args); - else + } else { finalArgs = args; + } AtomicReference retval = new AtomicReference<>(); AtomicReference retExcep = new AtomicReference<>(); @@ -182,12 +182,12 @@ public Object runKeyword(String keywordName, List args) { retval.set(super.runKeyword(keywordName, finalArgs)); return true; - } catch (JavaFXLibraryTimeoutException jfxte){ + } catch (JavaFXLibraryTimeoutException jfxte) { // timeout already expired, catch exception and jump out retExcep.set(jfxte); throw jfxte; - } catch (RuntimeException e){ + } catch (RuntimeException e) { // catch exception and continue trying retExcep.set(e); return false; @@ -227,17 +227,16 @@ public String getKeywordDocumentation(String keywordName) { return "IOException occurred while reading the documentation file!"; } } else if (keywordName.equals("__init__")) { - try { + try { return FileUtils.readFileToString(new File("./src/main/java/libdoc-init-documentation.txt"), "utf-8"); } catch (IOException e) { e.printStackTrace(); return "IOException occurred while reading the init documentation file!"; } } else { - try { + try { return super.getKeywordDocumentation(keywordName); - } - catch (Exception e) { + } catch (Exception e) { return keywordName; } } @@ -258,6 +257,27 @@ public static JavaFXLibrary getLibraryInstance() throws ScriptException { } public static void main(String[] args) throws Exception { + startServer(args); + } + + public static void premain(String args) { + TestFxAdapter.isAgent = true; + Thread agentThread = new Thread(() -> { + try { + if (args == null) { + startServer(); + } else { + startServer(args); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + agentThread.setDaemon(true); + agentThread.start(); + } + + public static void startServer(String... args) throws Exception { int port = 8270; InetAddress ipAddr = InetAddress.getLocalHost(); @@ -266,8 +286,7 @@ public static void main(String[] args) throws Exception { System.out.println("----------------------------= JavaFXLibrary =-----------------------------"); if (args.length > 0) { port = Integer.parseInt(args[0]); - } - else { + } else { System.out.println("RemoteServer for JavaFXLibrary will be started at default port of: " + port + ".\n" + "If you wish to use another port, restart the library and give port number\n" + "as an argument."); @@ -294,4 +313,3 @@ public static void main(String[] args) throws Exception { } } } - diff --git a/src/main/java/javafxlibrary/keywords/AdditionalKeywords/ApplicationLauncher.java b/src/main/java/javafxlibrary/keywords/AdditionalKeywords/ApplicationLauncher.java index 6c6a117..eae81ec 100644 --- a/src/main/java/javafxlibrary/keywords/AdditionalKeywords/ApplicationLauncher.java +++ b/src/main/java/javafxlibrary/keywords/AdditionalKeywords/ApplicationLauncher.java @@ -45,8 +45,8 @@ public class ApplicationLauncher extends TestFxAdapter { + "Example:\n" + "| Launch JavaFX Application | _javafxlibrary.testapps.MenuApp_ |\n" + "| Launch JavaFX Application | _TestApplication.jar_ |\n") - @ArgumentNames({"appName", "*args"}) - public void launchJavafxApplication(String appName, String... appArgs) { + @ArgumentNames({ "appName", "*args" }) + public void launchJavafxApplication(String appName, String... appArgs) { try { RobotLog.info("Starting application:" + appName); createNewSession(appName, appArgs); @@ -65,7 +65,7 @@ public void launchJavafxApplication(String appName, String... appArgs) { + "Example:\n" + "| Launch Swing Application | _javafxlibrary.testapps.SwingApplication |\n" + "| Launch Swing Application | _TestApplication.jar_ |\n") - @ArgumentNames({"appName", "*args"}) + @ArgumentNames({ "appName", "*args" }) public void launchSwingApplication(String appName, String... appArgs) { RobotLog.info("Starting application:" + appName); Class c = getMainClass(appName); @@ -84,7 +84,7 @@ public void launchSwingApplication(String appName, String... appArgs) { + "Example:\n" + "| Launch Swing Application In Separate Thread | _javafxlibrary.testapps.SwingApplication |\n" + "| Launch Swing Application In Separate Thread | _TestApplication.jar_ |\n") - @ArgumentNames({"appName", "*args"}) + @ArgumentNames({ "appName", "*args" }) public void launchSwingApplicationInSeparateThread(String appName, String... appArgs) { RobotLog.info("Starting application:" + appName); Class c = getMainClass(appName); @@ -95,10 +95,11 @@ public void launchSwingApplicationInSeparateThread(String appName, String... app private Class getMainClass(String appName) { try { - if (appName.endsWith(".jar")) + if (appName.endsWith(".jar")) { return getMainClassFromJarFile(appName); - else + } else { return Class.forName(appName); + } } catch (ClassNotFoundException e) { throw new JavaFXLibraryNonFatalException("Unable to launch application: " + appName, e); } @@ -112,41 +113,43 @@ private void _addPathToClassPath(String path) { try { Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); method.setAccessible(true); - method.invoke(classLoader, (new File(path)).toURI().toURL() ); + method.invoke(classLoader, (new File(path)).toURI().toURL()); } catch (Exception e) { - throw new JavaFXLibraryFatalException("Problem setting the classpath: " + path , e); + throw new JavaFXLibraryFatalException("Problem setting the classpath: " + path, e); } } @RobotKeyword("Loads given path to classpath.\n\n" - + "``path`` is the path to add.\n\n" - + "If directory path has asterisk(*) after directory separator all jar files are added from directory.\n" - + "\nExample:\n" - + "| Set To Classpath | C:${/}users${/}my${/}test${/}folder | \n" - + "| Set To Classpath | C:${/}users${/}my${/}test${/}folder${/}* | \n") - @ArgumentNames({"path"}) - public void setToClasspath(String path) { + + "``path`` is the path to add.\n\n" + + "If directory path has asterisk(*) after directory separator all jar files are added from directory.\n" + + "\nExample:\n" + + "| Set To Classpath | C:${/}users${/}my${/}test${/}folder | \n" + + "| Set To Classpath | C:${/}users${/}my${/}test${/}folder${/}* | \n") + @ArgumentNames({ "path" }) + public void setToClasspath(String path) { if (path.endsWith("*")) { - path = path.substring(0, path.length() - 1); - RobotLog.info("Adding all jars from directory: " + path); + path = path.substring(0, path.length() - 1); + RobotLog.info("Adding all jars from directory: " + path); - try { - File directory = new File(path); - File[] fileList = directory.listFiles(); - boolean jarsFound = false; - for (File file : Objects.requireNonNull(fileList)) { - if (file.getName().endsWith(".jar")) { - jarsFound = true; - _addPathToClassPath(file.getAbsolutePath()); - } - } - if(!jarsFound) throw new JavaFXLibraryNonFatalException("No jar files found from classpath: " + FileSystems.getDefault().getPath(path).normalize().toAbsolutePath().toString()); - } catch (NullPointerException e) { - throw new JavaFXLibraryFatalException("Directory not found: " + path + "\n" + e.getMessage(), e); - } - } - else { + try { + File directory = new File(path); + File[] fileList = directory.listFiles(); + boolean jarsFound = false; + for (File file : Objects.requireNonNull(fileList)) { + if (file.getName().endsWith(".jar")) { + jarsFound = true; + _addPathToClassPath(file.getAbsolutePath()); + } + } + if (!jarsFound) { + throw new JavaFXLibraryNonFatalException("No jar files found from classpath: " + + FileSystems.getDefault().getPath(path).normalize().toAbsolutePath().toString()); + } + } catch (NullPointerException e) { + throw new JavaFXLibraryFatalException("Directory not found: " + path + "\n" + e.getMessage(), e); + } + } else { _addPathToClassPath(path); } } @@ -157,8 +160,9 @@ public void logApplicationClasspath() { ClassLoader cl = ClassLoader.getSystemClassLoader(); URL[] urls = ((URLClassLoader) cl).getURLs(); RobotLog.info("Printing out classpaths: \n"); - for (URL url : urls) + for (URL url : urls) { RobotLog.info(url.getFile()); + } } catch (Exception e) { throw new JavaFXLibraryNonFatalException("Unable to log application classpaths", e); } @@ -237,7 +241,6 @@ public void clearObjectMap() { objectMap.clear(); } - @RobotKeyword("Returns the class name of currently active JavaFX Application") public String getCurrentApplication() { try { @@ -258,4 +261,9 @@ public String currentApplication() { } } + @RobotKeyword("Returns if JavaFXLibrary is started as java agent.") + public boolean isJavaAgent() { + return TestFxAdapter.isAgent; + } + } diff --git a/src/main/java/javafxlibrary/utils/Session.java b/src/main/java/javafxlibrary/utils/Session.java index de9e0d0..e415c82 100644 --- a/src/main/java/javafxlibrary/utils/Session.java +++ b/src/main/java/javafxlibrary/utils/Session.java @@ -18,9 +18,11 @@ package javafxlibrary.utils; import javafx.application.Application; +import javafx.collections.ObservableList; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; import javafx.stage.Stage; +import javafx.stage.Window; import javafxlibrary.exceptions.JavaFXLibraryNonFatalException; import org.testfx.api.FxRobot; import org.testfx.api.FxToolkit; @@ -28,6 +30,11 @@ import javax.swing.*; import java.awt.*; import java.awt.event.WindowEvent; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; +import java.util.Optional; import java.util.concurrent.TimeoutException; public class Session { @@ -42,7 +49,7 @@ public Session(String appName, String... appArgs) { try { // start the client this.primaryStage = FxToolkit.registerPrimaryStage(); - this.sessionApplication = FxToolkit.setupApplication((Class)Class.forName(appName), appArgs); + this.sessionApplication = FxToolkit.setupApplication((Class) Class.forName(appName), appArgs); this.sessionRobot = new FxRobot(); this.applicationName = appName; this.screenshotDirectory = System.getProperty("user.dir") + "/report-images/"; @@ -80,6 +87,24 @@ public Session(Application application) { } + /** + * Used when JavaFXLibrary is attached with java agent + */ + public Session(String applicationName) { + try { + Optional existingStage = getExistingPrimaryStage(); + if (!existingStage.isPresent()) { + throw new JavaFXLibraryNonFatalException("Could not hook to existing application: stage not found"); + } + this.primaryStage = FxToolkit.registerStage(existingStage::get); + this.sessionRobot = new FxRobot(); + this.applicationName = applicationName; + this.screenshotDirectory = System.getProperty("user.dir") + "/report-images/"; + } catch (Exception e) { + throw new JavaFXLibraryNonFatalException("Problem launching the application: " + e.getMessage(), e); + } + } + public void closeApplication() { try { FxToolkit.hideStage(); @@ -106,4 +131,42 @@ public void closeSwingApplication() { closeApplication(); } + + /** + * When running JavaFXLibrary as java agent this method tries to find first showing stage. + */ + private Optional getExistingPrimaryStage() { + + try { + ObservableList windows; + // getWindows method is added in Java 9 + windows = (ObservableList) Window.class.getMethod("getWindows") + .invoke(null); + return windows.stream() + .filter(Stage.class::isInstance) + .map(Stage.class::cast) + .filter(Stage::isShowing) + .findFirst(); + } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException | NoSuchMethodException e) { + // java 8 implementation + try { + Iterator it = (Iterator) Window.class.getMethod("impl_getWindows") + .invoke(null); + List windows = new ArrayList<>(); + while (it.hasNext()) { + windows.add(it.next()); + } + return windows.stream() + .filter(Stage.class::isInstance) + .map(Stage.class::cast) + .filter(Stage::isShowing) + .findFirst(); + } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | SecurityException ex) { + e.printStackTrace(); + } + + } + return Optional.empty(); + } } diff --git a/src/main/java/javafxlibrary/utils/TestFxAdapter.java b/src/main/java/javafxlibrary/utils/TestFxAdapter.java index 9220d86..59633ee 100644 --- a/src/main/java/javafxlibrary/utils/TestFxAdapter.java +++ b/src/main/java/javafxlibrary/utils/TestFxAdapter.java @@ -28,13 +28,18 @@ public class TestFxAdapter { - public static boolean isHeadless = false; + public static boolean isHeadless = false; + public static boolean isAgent = false; // current robot instance in use protected static FxRobotInterface robot; + public static void setRobot(FxRobotInterface robot) { TestFxAdapter.robot = robot; } - public static FxRobotInterface getRobot() { return robot; } + + public static FxRobotInterface getRobot() { + return robot; + } // TODO: consider adding support for multiple sessions private static Session activeSession = null; @@ -45,23 +50,36 @@ public static void setRobot(FxRobotInterface robot) { public static HashMap objectMap = new HashMap(); public void createNewSession(String appName, String... appArgs) { - - /* Applications using FXML-files for setting controllers must have - FXMLLoader.setDefaultClassLoader(getClass().getClassLoader()); - in their start method for the controller class to load properly */ - if (appName.endsWith(".jar")) { - Class mainClass = getMainClassFromJarFile(appName); - activeSession = new Session(mainClass, appArgs); + if (isAgent) { + createNewSession(appName.endsWith(".jar") ? getMainClassFromJarFile(appName).toString() : appName); } else { - activeSession = new Session(appName, appArgs); + /* + * Applications using FXML-files for setting controllers must have + * FXMLLoader.setDefaultClassLoader(getClass().getClassLoader()); in their start method for the controller + * class to load properly + */ + if (appName.endsWith(".jar")) { + Class mainClass = getMainClassFromJarFile(appName); + activeSession = new Session(mainClass, appArgs); + } else { + activeSession = new Session(appName, appArgs); + } + + setRobot(activeSession.sessionRobot); } - - setRobot(activeSession.sessionRobot); - } public void createNewSession(Application application) { - activeSession = new Session(application); + if (isAgent) { + createNewSession("JavaFXLibrary SwingWrapper"); + } else { + activeSession = new Session(application); + setRobot(activeSession.sessionRobot); + } + } + + private void createNewSession(String applicationName) { + activeSession = new Session(applicationName); setRobot(activeSession.sessionRobot); } @@ -74,26 +92,29 @@ public void deleteSwingSession() { } public String getCurrentSessionApplicationName() { - if (activeSession != null) + if (activeSession != null) { return activeSession.applicationName; + } return null; } public String getCurrentSessionScreenshotDirectory() { - if (activeSession != null) + if (activeSession != null) { return activeSession.screenshotDirectory; - else + } else { throw new JavaFXLibraryNonFatalException("Unable to get screenshot directory, no application is currently open!"); + } } - public void setCurrentSessionScreenshotDirectory(String dir){ + public void setCurrentSessionScreenshotDirectory(String dir) { if (activeSession != null) { File errDir = new File(dir); - if (!errDir.exists()) - if(!errDir.mkdirs()) { + if (!errDir.exists()) { + if (!errDir.mkdirs()) { RobotLog.warn("Screenshot directory \"" + dir + "\" creation failed!"); } + } activeSession.screenshotDirectory = dir; } else { throw new JavaFXLibraryNonFatalException("Unable to set screenshot directory, no application is currently open!"); diff --git a/src/main/java/libdoc-documentation.txt b/src/main/java/libdoc-documentation.txt index eb31ed2..e5b91ba 100644 --- a/src/main/java/libdoc-documentation.txt +++ b/src/main/java/libdoc-documentation.txt @@ -47,6 +47,12 @@ Experimental headless mode can be activated in remote mode at the import time by | *Settings* | *Value* | | Library | Remote | http://localhost:8270 | ${True} | WITH NAME | JavaFXLibrary | +Experimental: Java agent support + +Library can be used as java agent. Launch application with `-javaagent:/path/to/javafxlibrary-.jar`. +Default port is 8270 and can be changed with adding `=` to java agent command. Only remote library is supported. +Using launch keyword is still required but instead of starting new application keyword initializes Stage for library. + == 3. Locating JavaFX Nodes == === 3.1 Locator syntax === JavaFXLibrary uses TestFX lookup queries as the default way of locating JavaFX Nodes in the UI. These queries are very diff --git a/src/test/robotframework/acceptance/MiscTests.robot b/src/test/robotframework/acceptance/MiscTests.robot index 3913494..c47866b 100644 --- a/src/test/robotframework/acceptance/MiscTests.robot +++ b/src/test/robotframework/acceptance/MiscTests.robot @@ -369,6 +369,12 @@ Get Scene (Window) ${result} Get Root Node Of ${scene} Should Be Equal ${target} ${result} +Is not Java agent + [Tags] smoke + Set Test Application javafxlibrary.testapps.TestBoundsLocation + ${IS_JAVA_AGENT} = Is Java Agent + Should Be Equal ${False} ${IS_JAVA_AGENT} + *** Keywords *** Setup All Tests Import JavaFXLibrary