Tuesday, October 30, 2012

Running Selenium Grid as a Service

If you have started running you Selenium suite in parallel or in a distributed environment you're probably using the Selenium Grid2. Now you don't want the console window open with the hub and node running on your build server so you'll want to wrap it and run it as a service.

To do this I've used the Java Service Wrapper around the selenium-server-standalone-2.25.0.jar, the documentation is pretty solid. I used the 4th method listed on the site, Method 4 - WrapperJarApp Integration.

If you follow the instructions you should end up with a directory structure like this:
----HubService
|   ----bin
|   |       InstallSeleniumHub.bat
|   |       SeleniumHub.bat
|   |       UninstallSeleniumHub.bat
|   |       wrapper.exe
|   |       
|   ----conf
|   |       wrapper.conf
|   |       
|   ----lib
|   |       selenium-server-standalone-2.25.0.jar
|   |       wrapper.dll
|   |       wrapper.jar
|   |       
|   ----logs
|           wrapper.log
|              
----NodeService
   ----bin
   |       InstallSeleniumNode.bat
   |       SeleniumNode.bat
   |       UninstallSeleniumNode.bat
   |       wrapper.exe
   |       
   ----conf
   |       wrapper.conf
   |       
   ----lib
   |       selenium-server-standalone-2.25.0.jar
   |       wrapper.dll
   |       wrapper.jar
   |       
   ----logs
           wrapper.log
The only real work is inside the wrapper.conf partially shown below:

HubService - partial wrapper.conf:

#********************************************************************
# Wrapper Java Properties
#********************************************************************
# Java Application
#  Locate the java binary on the system PATH:
wrapper.java.command=java
#  Specify a specific java binary:
#set.JAVA_HOME=/java/path
#wrapper.java.command=%JAVA_HOME%/bin/java

# Tell the Wrapper to log the full generated Java command line.
#wrapper.java.command.loglevel=INFO

# Java Main class.  This class must implement the WrapperListener interface
#  or guarantee that the WrapperManager class is initialized.  Helper
#  classes are provided to do this for you.  See the Integration section
#  of the documentation for details.
wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperJarApp

# Java Classpath (include wrapper.jar)  Add class path elements as
#  needed starting from 1
wrapper.java.classpath.1=../lib/wrapper.jar
wrapper.java.classpath.2=../lib/selenium-server-standalone-2.25.0.jar

# Java Library Path (location of Wrapper.DLL or libwrapper.so)
wrapper.java.library.path.1=../lib

# Java Bits.  On applicable platforms, tells the JVM to run in 32 or 64-bit mode.
wrapper.java.additional.auto_bits=TRUE

# Java Additional Parameters
#wrapper.java.additional.1=

# Initial Java Heap Size (in MB)
#wrapper.java.initmemory=3

# Maximum Java Heap Size (in MB)
#wrapper.java.maxmemory=64

# Application parameters.  Add parameters as needed starting from 1
wrapper.app.parameter.1=../lib/selenium-server-standalone-2.25.0.jar
wrapper.app.parameter.2=-role
wrapper.app.parameter.3=hub
Here are the full conf files, remember to rename to wrapper.conf:
Hub wrapper.com
Node wrapper.com

Now you should first run the SeleniumHub.bat to test the wrapper in a console window. If that is successful you can then run InstallSeleniumHub.bat and InstallSeleniumNode.bat to register the services.

The service will still not be running until you start it as follows:
Start -> Run -> services.msc


This should be all you need to do.

Thursday, October 18, 2012

Running Selenium Testing in Parallel with JUnit

Recently I have been asked to speed up our Selenium regression test suite (currently at 2 hours) by running tests in parallel. We are using Jenkins to kick off the ant task that will run the test suite that is configured in Junit.

There are 2 parts to getting this to work:
  •         The Selenium Grid2 Hub & Node
  •         Junit's parallel thread process

As usual my first task is to get a prototype running locally. I started by reading the Selenium Grid2 documentation which is pretty solid.

I downloaded the selenium-server-standalone-2.25.0.jar and set up a couple of bat scripts to start the grid.
  •     startHub.bat
    java -jar selenium-server-standalone-2.25.0.jar -role hub
  •     startNode.bat
    java -jar selenium-server-standalone-2.25.0.jar -role node  -hub http://localhost:4444/grid/register -browser browserName=firefox,maxInstances=5 -browser "browserName=internet explorer,maxInstances=5" -maxSession 5

Place these in the dir with the jar, run the hub, run the node and you should be ready to rock. The console can be viewed at: http://localhost:4444/grid/console


Lets look at the code required to send the tests to the grid using the remote web driver. For completeness I've also included the code for setting up a proxy server and a regular web driver if the hub is not running.

 /**
     * Returns an instance of the configured remote web driver pointing to a local web hub on port 4444
     *
     * This is for use in stage. If the hub is not running a regular web driver is returned
     *
     * @return WebDriver hub
     */
    private WebDriver getWebDriver() throws UnknownHostException {
        try {
            return new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), getDesiredCapabilities());
        //if the remote selenium hub is unavailable return a regular web driver
        } catch (MalformedURLException e) {
            return getLocalWebDriver();
        } catch (UnreachableBrowserException e) {
            return getLocalWebDriver();
        }
    }
  
    /**
     * Returns an instance of the configured web driver.
     *
     * This is for local testing
     *
     * @return WebDriver
     */
    private WebDriver getLocalWebDriver() throws UnknownHostException {
        if (useFirefox()) {
            return new FirefoxDriver(getDesiredCapabilities());
        } else {
            return new InternetExplorerDriver(getDesiredCapabilities());
        }
    }
  
    /**
     * Returns a DesiredCapabilities for either IE or Firefox.
     *
     * @return DesiredCapabilities for either IE or Firefox
     * @throws UnknownHostException
     */
    private DesiredCapabilities getDesiredCapabilities() throws UnknownHostException {
        if (useFirefox()) { //firefox
            DesiredCapabilities dc = DesiredCapabilities.firefox();
            FirefoxProfile profile = new FirefoxProfile();
            if (useProxy())
                dc.setCapability(CapabilityType.PROXY, getSeleniumProxy());
            if (getLocale() != null)
                profile.setPreference("intl.accept_languages", getLocale().toString());
            dc.setCapability(FirefoxDriver.PROFILE, profile);
            return dc;
        } else { //internet explorer
            DesiredCapabilities dc = DesiredCapabilities.internetExplorer();
            if (useProxy())
                dc.setCapability(CapabilityType.PROXY, getSeleniumProxy());
            return dc;
        }
    }
  
    /**
     * This will return a Selenium configured proxy pointing to a local proxy
     * on port 6666
     *
     * @param server
     * @return
     * @throws UnknownHostException
     */
    private Proxy getSeleniumProxy() throws UnknownHostException {
        Proxy proxy = new Proxy();
        proxy.setProxyType(Proxy.ProxyType.MANUAL);
        String proxyStr = String.format("%s:%d", InetAddress.getLocalHost().getCanonicalHostName(), proxyPort);
        proxy.setHttpProxy(proxyStr);
        proxy.setSslProxy(proxyStr);
        proxy.setNoProxy("someaddress");
        return proxy;
    }

The next step is to set up our build.xml ant task to kick of the tests in parallel.

Thanks to code cop I've come up with this partial build.xml:
    <target name="run-tests-dev-parallel4" depends="compile">
      <parallel threadcount="4">
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="1" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="2" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="3" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="4" />
          <param name="junit.division.num" value="4" />
        </antcall>
      </parallel>
    </target>
   
    <target name="run-tests-dev-parallel8" depends="compile">
      <parallel threadcount="8">
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="1" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="2" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="3" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="4" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="5" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="6" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="7" />
        </antcall>
        <antcall target="-junitThread">
          <param name="junit.division.total" value="8" />
          <param name="junit.division.num" value="8" />
        </antcall>
      </parallel>
    </target>
   
    <target name="-junitThread">
        <echo message="started thread ${junit.division.num} of ${junit.division.total}" />
        <junit fork="true" forkmode="perBatch" printsummary="on" haltonfailure="false" failureproperty="tests.failed" showoutput="true">
        <classpath refid="dev-classpath" />
        <formatter type="xml" />        
        <batchtest todir="${dev.test.results.dir}">
            <fileset dir="${dev.bin.dir}">
                <include name="**/*Test.class" />
                <custom classname="fully.qualified.DividingSelector" classpath="bin">
                  <param name="divisor" value="${junit.division.total}" />
                  <param name="part" value="${junit.division.num}" />
                </custom>
            </fileset>
        </batchtest>
      </junit>
      <echo message="ended thread ${junit.division.num} of ${junit.division.total}" />
      <fail if="tests.failed">
                        tests.failed=${tests.failed}
                        ***********************************************************
                        ***********************************************************
                        ****  One or more tests failed!  Check the output ...  ****
                        ***********************************************************
                        ***********************************************************
       </fail>
    </target>

The DividingSelector.java is as follows:
import java.io.File;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Parameter;
import org.apache.tools.ant.types.selectors.BaseExtendSelector;

/**
 * Split a fileset into the given number of parts.
 *
 */
public final class DividingSelector extends BaseExtendSelector {
   
    /**
     * The num of the current test class we are processing, 1 to totalTests
     */
    private int testNum;

    /**
     * Number of total threads to divide the tests between, 1 to totalThreads
     */
    private int totalThreads;

    /**
     * Current thread to run a test
     */
    private int curThread;

    public void setParameters(Parameter[] pParameters) {
        super.setParameters(pParameters);
        for (int j = 0; j < pParameters.length; j++) {
            Parameter p = pParameters[j];
            if ("divisor".equalsIgnoreCase(p.getName())) {
                totalThreads = Integer.parseInt(p.getValue());
            } else if ("part".equalsIgnoreCase(p.getName())) {
                curThread = Integer.parseInt(p.getValue());
            } else {
                throw new BuildException("unsupported parameter " + p.getName() + " with value " + p.getValue());
            }
        }
    }

    public void verifySettings() {
        super.verifySettings();
        if (totalThreads <= 0 || curThread <= 0) {
            throw new BuildException("parameter part or divisor not set");
        }
        if (curThread > totalThreads) {
            throw new BuildException("parameter part must be <= divisor");
        }
    }

    public boolean isSelected(File pBaseDir, String pRelative, File pFullPath) {
        //increment the test counter
        testNum++;
       
        //this is the thread that will run the current test
        int threadNum = testNum % totalThreads + 1;
        boolean result = threadNum == curThread;
           
        if (result)
            System.out.println("thread: " + curThread + " ,running: " + pRelative + " , testnum: " + testNum);

        return result;
    }

}

Once you have the targets working you can configure it in Jenkins to run the run-tests-dev-parallel8 or run-tests-dev-parallel4 target depending on what works better for your build server.


When you run the build you'll see in the console output junit spawning the 8 threads and sending them to the hub, the hub will then handle the distribution to the nodes running the actual tests.

run-tests-dev-parallel8:

-junitThread:
     [echo] started thread 6 of 8

-junitThread:
     [echo] started thread 7 of 8

-junitThread:
     [echo] started thread 5 of 8

-junitThread:
     [echo] started thread 1 of 8

-junitThread:
     [echo] started thread 8 of 8

-junitThread:
     [echo] started thread 2 of 8

-junitThread:
     [echo] started thread 3 of 8

-junitThread:
     [echo] started thread 4 of 8

It will then pre divide the tests between the threads and will printout which thread has been allocated to which test. This can cause problems of a long running thread which is left after everything else is finished. This can be build into the DividingSelector but in not a prefect solution.

Next we will look at running the hub and node as a windows service on the build server.

References:

http://blog.code-cop.org/2009/09/parallel-junit.html



Automating Websphere Deploy

Last couple of days I've been investigating automating our application deployments to Websphere Application Server 8 (WAS). With little WAS administration experience the learning curve was steep, and documentation was outdated most often for WAS 6, it seemed there were two ways to go using wsadmin with jython which seemed overly complex for my purposes and ws_ant which it the direction I choose.

First I needed to get a prototype working locally as I had no access to the dev server hosting our application. So I installed the 60 day WAS 8.5 trial on my machine. Even this was no easy task, the deafult is to try and download with the 'Download Director' well this didn't work so well with my proxy settings and crashed several times. I then found the second tab 'Download using http'. I also installed the IBM Installation Manager. Once the 3GB package was downloaded I unzipped them into a directory.



Next I fired up the Installation Manager, the first problem I had was this message:


Installation Manager cannot find any packages to install. In order to access packages you must configure a repository connection and ensure that you can access the network or your repository media.

I clicked the repositories link and the 'Add Reposiroty...' I didn't know exactly what this was looking for but pointing it to the unzipped WAS 8 directory file 'repository.config' seemed to do the trick. Following the prompts I soon had WAS 8.5 installed on my machine.



Next I started to get as much information about ws_ant as I could. I came up with the following ant script. Initially I added the targets to our usual build.xml but this didn't work so well, I'd reccommend you keep them seperate so I added a wasdeploy.xml with the following:
wasdeploy.xml
<?xml version="1.0"?>
<!DOCTYPE project>
<project name="wasdeploy" basedir=".">

    <!-- Project properties -->
    <property name="name" value="myapplication" />
    <property name="build.dir" location="build" />

    <!-- was properties -->
    <property name="hostName" value="localhost" />
    <property name="connType" value="SOAP" />
    <property name="connPort" value="8880" />
    <property name="userId" value="wasadmin" />
    <property name="password" value="wasadmin" />
    <property name="wasHome.dir" value="C:/Program Files/IBM/WebSphere/AppServer" />
    <property name="node" value="Node01" />
    <property name="server" value="server1" />

    <!-- Was Ant task definitions -->
    <taskdef name="wsStartApplication" classname="com.ibm.websphere.ant.tasks.StartApplication" />
    <taskdef name="wsStopApplication" classname="com.ibm.websphere.ant.tasks.StopApplication" />
    <taskdef name="wsStartServer" classname="com.ibm.websphere.ant.tasks.StartServer" />
    <taskdef name="wsStopServer" classname="com.ibm.websphere.ant.tasks.StopServer" />
    <taskdef name="wsInstallApp" classname="com.ibm.websphere.ant.tasks.InstallApplication" />
    <taskdef name="wsUninstallApp" classname="com.ibm.websphere.ant.tasks.UninstallApplication" />
    <taskdef name="wsListApps" classname="com.ibm.websphere.ant.tasks.ListApplications" />

    <!--
    other helpful properties
    wasHome="${wasHome.dir}"
    conntype="${connType}"
    port="${port}"
    host="${hostName}" ip address or remote was
    user="${userId}"
    password="${password}"
    -->

    <target name="listApps">
 <wsListApps />
    </target>

    <target name="deploy" description="Deploy the ear">
        <wsInstallApp ear="${build.dir}/${name}.ear" options="-appname ${name}" failonerror="true"/>
    </target>

    <target name="undeploy">
        <wsUninstallApp application="${name}" failonerror="true" />
    </target>

    <target name="startApplication">
        <wsStartApplication application="${name}" wasHome="${wasHome.dir}" server="${server}" node="${node}"/>
    </target>

    <target name="stopApplication">
        <wsStopApplication application="${name}" wasHome="${wasHome.dir}" server="${server}" node="${node}"/>
    </target>

    <target name="update" >
        <wsInstallApp ear="${build.dir}/${name}-qa.ear" options="-appname ${name} -update" failonerror="true"/> 
    </target>
   
    <target name="startServer">
        <wsStartServer server="${server}"
                       logFile="${build.dir}/start.log"
                       trace="false"
                       failonerror="false" />
    </target>

    <target name="stopServer">
        <wsStopServer server="${server}"
                      logFile="${build.dir}/stop.log"
                      trace="false"
                      failonerror="false" />
    </target>

</project>
The script is pretty self explanatory, it contains 8 targets, one for each taskdef as well as an update target. I also added the listApps as a simple sanity check as it has no arguments and will not update anything.

Note there is no username/password needed as I turned off security for my local install, but this is easy to add as shown in comments. Things like the connection type and port don't seem to be needed locally but will probably be used for remote deploy.

Some helpful tips:
  • WAS_HOME\profiles\AppSrv01\properties\wsadmin.properties - this will contain the scripting connection properties such as Soap over port 8880
  •  I found a lot of people talking about an outdated wsanttasks.jar containing the taskdef classes but these are now found in:WAS_HOME\plugins\com.ibm.ws.runtime.jar
  • The admin console can be found at:  http://localhost:9060/ibm/console/ (the WC_adminhost port)
  • The application can be found at: http://localhost:9080/myapplication (the WC_defaulthost port)
  • The deploy target will not start the application.
Running these ant tasks using ws_ant.bat is the next part.

I created some tools in Eclipse to call them using the 'External Tools Configurations' under the run menu.

The Location points to the ws_ant.bat script in WAS_HOME\profiles\AppSrv01\bin


Here the arguments simply specify the ant xml file and the last argument is the target.

This is the output you can expect from a successful deploy:

Buildfile: wasdeploy.xml

deploy:
[wsInstallApp] Installing Application [C:\svnroot\java\web\myapplication\branches\develop\build\myapplication.ear]...
  [wsadmin] WASX7209I: Connected to process "server1" on node Node01 using SOAP connector;  The type of process is: UnManagedProcess
  [wsadmin] ADMA5016I: Installation of myapplication started.
  [wsadmin] ADMA5058I: Application and module versions are validated with versions of deployment targets.
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5081I: The bootstrap address for client module is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5053I: The library references for the installed optional package are created.
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5001I: The application binaries are saved in C:\Program Files\IBM\WebSphere\AppServer\profiles\AppSrv01\wstemp\Script13a751c1a27\workspace\cells\Node01Cell\applications\myapplication.ear\myapplication.ear
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] SECJ0400I: Successfully updated the application myapplication with the appContextIDForSecurity information.
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5005I: The application myapplication is configured in the WebSphere Application Server repository.
  [wsadmin] ADMA5113I: Activation plan created successfully.
  [wsadmin] ADMA5011I: The cleanup of the temp directory for application myapplication is complete.
  [wsadmin] ADMA5013I: Application myapplication installed successfully.
  [wsInstallApp] Installed Application [C:\svnroot\java\web\myapplication\branches\develop\build\myapplication.ear]

BUILD SUCCESSFUL
Total time: 2 minutes 16 seconds


Part 2 of this will be running the ant task with Jenkins on a build server and deploying to a remote server hosting the application.

References: