Saturday, November 2, 2013

Get IP of Grid node when using Selenium2Library & Robot Framework

Sometimes it is useful to know which machine is executing a Selenium test when using Selenium Grid.

The following code was adapted from a post written in Java. I converted it into a keyword for Robot Framework and added code to grab the session ID from Selenium2Library.

 import urllib2, json  
 from robot.libraries.BuiltIn import BuiltIn  
 from robot.api import logger  
 from urlparse import urlparse  

 def get_node_hostname(hub_url):  
    '''Returns the hostname/IP of the node executing the session belonging to the active browser.'''  
     
    s2l = BuiltIn().get_library_instance('Selenium2Library')  
    session_id = s2l._current_browser().session_id  
    parse_result = urlparse(hub_url)  
    host = parse_result.hostname  
    port = parse_result.port  
    query_url = 'http://%s:%s/grid/api/testsession?session=%s' % (host, port, session_id)  
    req = urllib2.Request(url=query_url)  
    resp = urllib2.urlopen(req).read()  
    json_blob = json.loads(resp)  
    if 'proxyId' in json_blob:  
        proxy_id = json_blob['proxyId']  
        print '*INFO* proxyId is %s' % proxy_id  
        parse_result = urlparse(proxy_id)  
        return parse_result.hostname  
    else:  
        raise RuntimeError('Failed to get hostname. Is Selenium running locally? hub response: %s' % resp)

Using the Copy Artifact Plugin in a system Groovy script

When Groovy Plugin for Jenkins added build context variables (build, launcher, listener) to the script environment in version 1.13, it gave users the power to do great things in a Groovy script. We have a build that has numerous builds that are triggered by a parent job. Some jobs may not be triggered on every build. The nicest solution was to iterate over the projects that were built and copy their artifacts so that the test results could be aggregated.

One of the difficulties in writing this was dealing with Groovy's dispatch. Jenkins has a compatibility layer to keep older plugins working with a newer version of Jenkins. When written as you would write the code in Java, Groovy selected the deprecated overload of perform regardless of the argument types used in the call. All the deprecated overload did was throw an exception. Groovy looks at the run-time type of arguments and does not care about the declared or reference type of the argument you are using. In this case, Groovy's logic made the deprecated method appear to be the best choice when there were multiple overloads with compatible parameters. Casting did not help here.

This was overcome by using reflection to manually pick the newer method. It is ugly but it works.

Below you can see an example of a system Groovy Script that repeatedly invokes the Copy Artifact Plugin to aggregate the artifacts of jobs triggered with the Parameterized Trigger Plugin. The list of jobs that were built is stored as a comma-delimited list in the build variable PROJS prior to executing this.

 // copy the output from all the triggered jobs by invoking the Copy Artifact plugin for the  
 // builds that were triggered  
 // Assumptions: test output is in test-output folder, output and report files are  
 // named output.xml and report.html  
 import hudson.plugins.copyartifact.SpecificBuildSelector  
 import hudson.plugins.copyartifact.CopyArtifact  
 import hudson.model.AbstractBuild  
 import hudson.Launcher  
 import hudson.model.BuildListener  
 import hudson.FilePath  
   
 def envVars= build.getEnvironment()  
 def projs = envVars.get("PROJS")  
   
 projs.split(",").each {  
   println "Fetching output from " + it + "..."  
   copyTriggeredResults(it)  
 }  
   
 def copyTriggeredResults(projName) {  
   def buildNbr = build.getEnvironment().get("TRIGGERED_BUILD_NUMBER_" + projName)  
   def selector = new SpecificBuildSelector(buildNbr)  
   
   // CopyArtifact(String projectName, String parameters, BuildSelector selector,  
   // String filter, String target, boolean flatten, boolean optional)  
   def copyArtifact = new CopyArtifact(projName, selector, "**/*.*", null, false, false)  
   
   // use reflection because direct call invokes deprecated method  
   // perform(Build<?, ?> build, Launcher launcher, BuildListener listener)  
   def perform = copyArtifact.class.getMethod("perform", AbstractBuild, Launcher, BuildListener)  
   perform.invoke(copyArtifact, build, launcher, listener)  
   
   // rename test output to be output_${project name}.xml  
   def target = new FilePath(build.workspace, "test-output/output_" + projName + ".xml")  
   def source = new FilePath(build.workspace, "test-output/output.xml")  
   source.renameTo(target)  
 }