Writing a Robot Remote Library

After reading the quick introduction to Robot Framework, you might have been asking yourself “so, what else can I do with this Robot thing?” – well there are, of course, a lot of things you can do with it.  Today, I want to take a look at writing Remote Libraries.

Robot Framework is extensible through a library mechanism.  You can write libraries in Java or Python, or you can wrtie a remote library, which just has to be an XMLRPC server – the implementation language is not important.

One thing that I think we are going to want in the future, when we start provisioning environments using tools like Chef, is a way to check that we got what we wanted.  So, I decided to start writing a little Robot library that we could use to check that an environment is configured the way we think it is, or expect it to be.  Obviously there are other ways to do this, perhaps better ways, like exposing the configuration through Ohai for example, but I wanted to write a Robot Remote Library, and this seemed like a reasonable example.

So what will our library do?  Well, we will build it as a simple Web Application (WAR) that we will deploy on to our WebLogic Admin Server, and we will allow it to answer some simple questions like:

  • Does the server called ‘AdminServer’ exist?
  • Is the application called ‘Something’ in the state ‘RUNNING’?
  • Does the data source called ‘Something’ exist?
  • and so on…

You could imagine that we could go on extending this to let us ask questions about SOA, OSB, or anything running on WebLogic Server.  To answer the questions, we will just go read information out of MBeans, and since Fusion Middleware products conveniently put configuration information in MBeans, it is relatively straightforward for us to go get it.

And since we are building a Robot library, let’s also use Robot to test it!  We will build it with Maven, and use the Maven Robot Framework plugin to execute our tests.

So, before we get started, let’s take a quick look at how remote libraries work. You might want to take a quick look at some of the documentation here.

But what we need to know right now is the following:

  • Robot will make calls to the remote library using XML-RPC over HTTP
  • It expects (requires) there to be no prefix on the keywords
  • It will first call get_keyword_names to get a list of the keywords that the library supports
  • When you ask it to do something, it will call run_keyword to run the keyword you asked for

Keywords (you may recall) are used in the test case to express what must be done to execute the test.  So, let’s do a little test driven development, and start with our test case:

*** Settings ***
Library  Remote  http://localhost:7001/oracleRobot/xmlrpc

*** Test Cases ***

Ping Test
   ${pong} =                        ping              bob
   Should Be Equal as Strings       ${pong}           pong bob

Server State Test
   Server Status Should be          AdminServer       RUNNING

Application State Test
   Application Status Should Be     oracleRobot       ACTIVE

Data Source Existence Test
   Data Source Should Exist         myDataSource

You can see that it tells Robot the URL to call the XMLRPC Remote Library in the Settings section.  The keyword ‘Remote’ tells it that is it a remote (as opposed to local or built-in) library.

In the test cases you can see the keywords we are going to define, and also surmise their arguments and return values:

Keyword Arguments Return Value
ping a message the same message
Server Status Should Be server name, state
Application Status Should Be application name, state
Data Source Should Exist data source name

Some of them don’t seem to have return values – that is because the protocol has a built in set of return values to tell Robot if the test was successful or failed (plus some more information about failures – we’ll see this later on), so we don’t need to define our our output to indicate success.

At this point, if you want to follow along with the code, you can grab it from java.net using git:

git clone git://java.net/ci4fmw~robot

Ok, let’s set about building our project.  First we will create a Maven POM to describe the project and handle all of the dependencies we need.

First, we are going to need three dependencies:

  • com.oracle.weblogic:weblogic-server-pom:12.1.2-0-0, type=pom, scope=provided
  • org.apache.xmlrpc:xmlrpc-server:3.1.3
  • org.apache.xmlrpc:xmlrpc-common:3.1.3

You will need to populate your Maven repository with the WebLogic Server artifacts using the Maven Synchronization Plugin.  You can find details in the documentation here, and an example in this earlier post/video.

Next, we can set up our build section.  We need to tell the maven-compiler-plugin to use Java 1.6, we need to configure the maven-war-plugin to create the WAR file, and we need to set up the weblogic-maven-plugin to deploy our WAR to our WebLogic Server instance for testing.

Finally, we need to tell Maven to grab the robotframework-maven-plugin and run the test suite.

Here is what our finished POM looks like:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.oracle.maven</groupId>
  <artifactId>oracle-robot</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>oracleRobot</name>
  <properties>
    <serverUrl>t3://localhost:7001</serverUrl>
    <serverName>AdminServer</serverName>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.oracle.weblogic</groupId>
      <artifactId>weblogic-server-pom</artifactId>
      <version>12.1.2-0-0</version>
      <type>pom</type>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.xmlrpc</groupId>
      <artifactId>xmlrpc-server</artifactId>
      <version>3.1.3</version>
    </dependency>
    <dependency>
      <groupId>org.apache.xmlrpc</groupId>
      <artifactId>xmlrpc-common</artifactId>
      <version>3.1.3</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>oracleRobot</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.1.1</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.oracle.weblogic</groupId>
        <artifactId>weblogic-maven-plugin</artifactId>
        <version>12.1.2-0-0</version>
        <configuration>
        </configuration>
        <executions>
          <execution>
            <id>wls-deploy</id>
            <phase>pre-integration-test</phase>
            <goals>
              <goal>deploy</goal>
            </goals>
            <configuration>
              <adminurl>${serverUrl}</adminurl>
              <user>weblogic</user>
              <password>welcome1</password>
              <source>${project.build.directory}/${project.build.finalName}.${project.packaging}</source>
              <targets>${serverName}</targets>
              <verbose>true</verbose>
              <stage>true</stage>
              <upload>true</upload>
              <name>${project.build.finalName}</name>
            </configuration>
          </execution>
       </executions>
       <dependencies>
       </dependencies>
      </plugin>
      <plugin>
        <groupId>org.robotframework</groupId>
        <artifactId>robotframework-maven-plugin</artifactId>
        <version>1.1</version>
        <executions>
          <execution>
            <goals>
              <goal>run</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

Now, as we know from this earlier post we place the test suite in a directory called src/test/robotframework/acceptance.  I called mine mytest.txt.  The full source for the test is just up above.

Next, we are going to need a web.xml for the Web Application, so let’s create that.  It goes in src/main/webapp/WEB-INF and here is what it looks like:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" >

  <session-config>
    <session-timeout>30</session-timeout>
  </session-config>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
  </welcome-file-list>
  <servlet>
    <servlet-name>XmlRpcServlet</servlet-name>
    <servlet-class>com.oracle.maven.MyXmlRpcServlet</servlet-class>
    <init-param>
      <param-name>enabledForExtensions</param-name>
      <param-value>true</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>XmlRpcServlet</servlet-name>
    <url-pattern>/xmlrpc</url-pattern>
  </servlet-mapping>
</web-app>

Here we are telling it the name of the servlet we will use to handle the XML-RPC requests and mapping it to the url /xmlrpc.  Notice that we also set enabledForExtensions to true, because we are going to define our our handler since the library we are using assumes prefixes are required, but we actually do not want prefixes.

Let’s look at the servlet – here is the code:

package com.oracle.maven;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.server.XmlRpcHandlerMapping;
import org.apache.xmlrpc.webserver.XmlRpcServlet;

/**
 *  An XMLRPC servlet.
 *  The Robot Framework allows for the definition of a remote library
 *  using an XML RPC protocol. In this class, we implement an XMLRPC
 *  server as a servlet.  The Apache XMLPRC library that we are using
 *  creates services with a prefix, e.g. Robot.ping, however Robot
 *  expects there to be no prefix, e.g. just ping, so we need to add
 *  a little logic to remove the prefixes.
 */
public class MyXmlRpcServlet extends XmlRpcServlet  {

  /**
   *  Register a new <code>HandlerMapping</code> and remove prefixes.
   */
  @Override
  protected XmlRpcHandlerMapping newXmlRpcHandlerMapping() throws XmlRpcException {
    HandlerMapping mapping = new HandlerMapping();
    mapping.addHandler(com.oracle.maven.Robot.class.getName(), com.oracle.maven.Robot.class);
    mapping.removePrefixes();
    return mapping;
  }

}

Basically all we are doing here is registering our custom handler that removes the prefixes so that our XML-RPC services look the way Robot wants them to look.

Let’s take a look at that handler, here is the source:

Please note that this was adapted from here and is licensed under the Apache License, Version 2.0 – see the source file for more details.

package com.oracle.maven;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.server.AbstractReflectiveHandlerMapping;

/**
 *  A <code>HandlerMapping</code> that removes the prefixes of service names.
 */
public class HandlerMapping extends AbstractReflectiveHandlerMapping {

    /**
     * Removes the prefixes from all keys in this handler mapping assuming a
     * String was used as the key and period was
     * used as a separator. Example: Robot.getInvoice -> getInvoice
     */
    @SuppressWarnings("unchecked")
    public void removePrefixes() {
      Map<String, Object> newHandlerMap = new HashMap<String, Object>();
      for (Entry<String, Object> entry : (Set<Entry<String, Object>>) this.handlerMap.entrySet()) {
        String newKey = (String) entry.getKey();
        if (entry.getKey() instanceof String) {
          String key = (String) entry.getKey();
          if (key.contains(".")) {
            newKey = key.substring(key.lastIndexOf(".") + 1);
          }
        }
        newHandlerMap.put(newKey, entry.getValue());
      }
      this.handlerMap = newHandlerMap;
    }

    /**
     * Adds handlers for the given object to the mapping. The handlers are build by invoking
     * {@link #registerPublicMethods(String, Class)}.
     *
     * @param pKey
     *            The class key, which is passed to {@link #registerPublicMethods(String, Class)}.
     * @param pClass
     *            Class, which is responsible for handling the request.
     */
    public void addHandler(String pKey, Class<?> pClass) throws XmlRpcException {
      registerPublicMethods(pKey, pClass);
    }
}

Basically this is just going to go through the service names, which will something like Robot.MyService, and remove the prefix, so that it is just MyService.  We need to do this because that is the way Robot expects the services to be.  These two classes (the servlet and the handler) are actually generic – you should not need to make any changes to them (other than the class names in the servlet) to use them in another Robot Remote Library.

Now, we get to the class that does all the work.  The one that actually implements the keywords we will be using.  Here is the code:

package com.oracle.maven;

import java.util.Map;
import java.util.HashMap;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.MalformedObjectNameException;
import javax.naming.InitialContext;

/**
 *  A Remote Library for Robot Framework that defines keywords for Fusion Middleware.
 *  This is remote library for the Robot Framework, which can be deployed on a WebLogic
 *  Server and can be used in Robot Test Suites to obtain information about the
 *  WebLogic Domain.  This is mainly useful for 'infrastructure' level tests, for
 *  example, it will answer questions like:
 *  <p/>
 *  <ul>
 *  <li>Does a given server have a given state?</li>
 *  <li>Is a given application deployed?</li>
 *  <li>Does a given DataSource exist?</li>
 *  </ul>
 *  <p/>
 *  This remote library uses WebLogic MBeans, and so it must be deployed on the
 *  AdminServer, not a managed server.
 *  The Robot protocol uses xmlrpc with no prefixes.
 *  <p/>
 *  How to use:
 *  <p/>
 *  Suppose this robot is deployed on context root /oracleRobot.
 *  In your Test Suite, you access it by declaring a remote library in the Settings section
 *  then you may use keywords from this library in your Test Cases section.
 *  For example:
 *  <p/>
 *  <pre>
 *  *** Settings ***
 *  Library  Remote  http://server:port/oracleRobot
 *
 *  *** Test Cases ***
 *  Server Status Should Be   AdminServer    RUNNING
 *  Application Should Be     myWebApp       Active
 *  Data Source Should Exist  myDataSource
 *  </pre>
 *
 *  <p/>
 *  Robot looks for keywords by removing capitals and replacing spaces with
 *  underscores, so in the example above, when we say 'Server Status Should Be',
 *  it will call an XMLRPC method names 'server_status_should_be'.
 *  <p/>
 *  Note that Robot is a Python-based framework, and hence this class contains
 *  method names that follow the Python, rather than Java, naming conventions.
 */
public class Robot {

  private static final ObjectName domainRuntime;
  private static final ObjectName runtime;

  // string literals
  private static final String STATUS    = "status";
  private static final String ERROR     = "error";
  private static final String TRACEBACK = "traceback";
  private static final String RETURN    = "return";
  private static final String EMPTY     = "";
  private static final String PASS      = "PASS";
  private static final String FAIL      = "FAIL";

  private static final String NYI       = "Not Yet Implemented";

  // Initializing the object name for DomainRuntimeServiceMBean
  // so it can be used throughout the class.
  static {
    try {
      domainRuntime = new ObjectName(
          "com.bea:Name=DomainRuntimeService,Type=weblogic.management.mbeanservers.domainruntime.DomainRuntimeServiceMBean");
    } catch (MalformedObjectNameException e) {
      e.printStackTrace();
      throw new AssertionError(e.getMessage());
    }
  }

  // Initializing the object name for RuntimeServiceMBean
  // so it can be used throughout the class.
  static {
    try {
      runtime = new ObjectName(
          "com.bea:Name=RuntimeService,Type=weblogic.management.mbeanservers.runtime.RuntimeServiceMBean");
    } catch (MalformedObjectNameException e) {
      e.printStackTrace();
      throw new AssertionError(e.getMessage());
    }
  }

  //
  // these are the methods that implement Robot keywords
  //

  /**
   *  A Robot keyword that simply returns a message acknowledging its existense.
   *  Mainly to be used to check that the remote robot library is functioning.
   *
   *  @param args An <code>Object[1]</code> where <code>args[0]</code> contains
   *              a message to be echoed back.
   *  @return The robot protocol formatted return object containing the message.
   */
  public HashMap<String, Object> ping(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    Object returnObj = "pong " + args[0];
    // create the response
    result.put(STATUS, PASS);
    result.put(ERROR, EMPTY);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, returnObj);
    return result;
  }

  /**
   *  A Robot keyword that checks that a Data Source exists.
   *
   *  @param args An <code>Object[1]</code> where <code>args[0]</code> contains
   *              the name of the DataSource you want to confirm exists.
   *  @return The robot protocol formatted return object containing status of
   *          pass if the DataSource exists or fail otherwise.
   */
  public HashMap<String, Object> data_source_should_exist(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    Object returnObj = EMPTY;
    // create the response
    result.put(STATUS, PASS);
    result.put(ERROR, NYI);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, returnObj);
    return result;
  }

  /**
   *  A Robot keyword that checks the status of a server.
   *  This keyword allows you to check the status of a WebLogic Server against
   *  a state that you expect it to have.  If the server state matches your
   *  expectation, the keyword will pass, otherwise it will fail.
   *
   *  @param args An <code>Object[2]</code> where <code>args[0]</code> contains
   *              a the name of the server, e.g. <code>AdminServer</code> and
   *              <code>args[1]</code> contains the desired state, e.g.
   *              <code>RUNNING</code>.
   *  @return The robot protocol formatted return object indicating pass/fail.
   */
  public HashMap<String, Object> server_status_should_be(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    InitialContext ctx;
    boolean found = false;
    try {
      ctx = new InitialContext();
      MBeanServer server = (MBeanServer)ctx.lookup("java:comp/env/jmx/domainRuntime");
      ObjectName[] serverRuntimes = (ObjectName[]) server.getAttribute(domainRuntime, "ServerRuntimes");
      for (int i = 0; i < serverRuntimes.length; i++) {
        String serverName = (String) server.getAttribute(serverRuntimes[i], "Name");
        String serverState = (String) server.getAttribute(serverRuntimes[i], "State");
        if (((String)args[0]).compareTo(serverName) == 0) {
          // correct server
          if (((String)args[1]).compareTo(serverState) == 0 ) {
            // correct state
            found = true;
            break;
          }
        }
      }
    } catch (Exception e) {
      result.put(STATUS, FAIL);
      result.put(ERROR, e.getMessage());
      result.put(TRACEBACK, e.getStackTrace());
      result.put(RETURN, EMPTY);
      return result;
    }
    // create the response
    result.put(STATUS, found ? PASS : FAIL);
    result.put(ERROR, EMPTY);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, EMPTY);
    return result;
  }

  /**
   *  A Robot keyword that checks the status of an application.
   *  This keyword allows you to check the status of an application against
   *  a state that you expect it to have.  If the application state matches your
   *  expectation, the keyword will pass, otherwise it will fail.
   *  <p/>
   *  Allowed states are ACTIVE, ACTIVE_ADMIN and INACTIVE.
   *
   *  @param args An <code>Object[2]</code> where <code>args[0]</code> contains
   *              a the name of the application, e.g. <code>myWebApp</code> and
   *              <code>args[1]</code> contains the desired state, e.g.
   *              <code>ACTIVE</code>.
   *  @return The robot protocol formatted return object indicating pass/fail.
   */
  public HashMap<String, Object> application_status_should_be(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    InitialContext ctx;
    boolean found = false;
    try {
      // decode state
      int targetState = -1;
      if (((String)args[1]).compareTo("INACTIVE") == 0 ) {
        targetState = 0;
      } else if (((String)args[1]).compareTo("ACTIVE_ADMIN") == 0 ) {
        targetState = 1;
      } else if (((String)args[1]).compareTo("ACTIVE") == 0 ) {
        targetState = 2;
      }
      if (targetState == -1) {
        throw new Exception("You provided an invalid state.  Valid states are ACTIVE, ACTIVE_ADMIN and INACTIVE");
      }
      // now look up the application state
      ctx = new InitialContext();
      MBeanServer server = (MBeanServer)ctx.lookup("java:comp/env/jmx/runtime");
      ObjectName serverRuntime = (ObjectName) server.getAttribute(runtime, "ServerRuntime");
      ObjectName[] appRuntimes = (ObjectName[]) server.getAttribute(serverRuntime, "ApplicationRuntimes");
      for (int i = 0; i < appRuntimes.length; i++) {
        String appName = (String) server.getAttribute(appRuntimes[i], "ApplicationName");
        int appState = (Integer) server.getAttribute(appRuntimes[i], "ActiveVersionState");
        if (((String)args[0]).compareTo(appName) == 0) {
          // correct application
          if (appState == targetState) {
            // correct state
            found = true;
            break;
          }
        }
      }
    } catch (Exception e) {
      e.printStackTrace(System.out);
      result.put(STATUS, FAIL);
      result.put(ERROR, e.getMessage());
      result.put(TRACEBACK, e.getStackTrace());
      result.put(RETURN, EMPTY);
      return result;
    }
    // create the response
    result.put(STATUS, found ? PASS : FAIL);
    result.put(ERROR, EMPTY);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, EMPTY);
    return result;
  }

  //
  // methods required by the robotframework remote library protocol follow
  //

  /**
   *  Get a list of Robot keywords implemented in this remote library.
   *  This method is required by the Robot remote library protocol.
   *
   *  @return A <code>String[]</code> containing the names of the keywords
   *          that are implemented by this library.
   */
  public String[] get_keyword_names() {
    // TODO: change this to use reflection?
    return new String[] { "ping",
                          "server_status_should_be",
                          "application_status_should_be",
                          "data_source_should_exist"
                        };
  }

  /**
   *  Run the given keyword.
   *  This method is required by the Robot remote library protocol.
   *  <p/>
   *  The expected return object is a <code>Map</code> containing the
   *  following entries:
   *  <p/>
   *  <ul>
   *  <li><code>status</code>: Indicates if the keyword was successful, values
   *  are <code>PASS</code> or <code>FAIL</code>.</li>
   *  <li><code>return</code>: The (arbitrary) return object/message from the
   *  keyword.</li>
   *  <li><code>error</code>: The error message, if any.</li>
   *  <li><code>traceback</code>: Additional information, such as a stack trace,
   *  for diaganosing errors.</li>
   *  </ul>
   *
   *  @param keyword The name of the keyword to run.
   *  @param args The arguments for the keyword, in an <code>Object[]</code>.
   *  @return The result of executing the keyword, formatted as the Robot
   *          framework expects.
   */
  public HashMap<String, Object> run_keyword(String keyword, Object[] args) {
    HashMap<String, Object> kr = new HashMap<String, Object>();
    try {
      // run the right method
      // TODO: change this to use reflection perhaps?
      if (keyword.equalsIgnoreCase("ping")) {
        kr = ping(args);
      } else if (keyword.equalsIgnoreCase("server_status_should_be")) {
        kr = server_status_should_be(args);
      } else if (keyword.equalsIgnoreCase("application_status_should_be")) {
        kr = application_status_should_be(args);
      } else if (keyword.equalsIgnoreCase("data_source_should_exist")) {
        kr = data_source_should_exist(args);
      } else {
        kr.put(STATUS, FAIL);
        kr.put(RETURN, EMPTY);
        kr.put(ERROR, EMPTY);
        kr.put(TRACEBACK, EMPTY);
      }
    } catch (Exception e) {
      e.printStackTrace(System.out);
    }
    return kr;
  }

}

So let’s take a look at this class.  Right up top we define a few constants and some statics to refer to the roots of the DomainRuntimeService and RuntimeService JMX trees in WebLogic.

Then we move on to define our keywords.  The first one is a really simple ‘ping’:

  public HashMap<String, Object> ping(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    Object returnObj = "pong " + args[0];
    // create the response
    result.put(STATUS, PASS);
    result.put(ERROR, EMPTY);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, returnObj);
    return result;

This shows us how the Robot protocol works.  First of all, we are going to receive the inputs in an array of Objects, and we are going to send the result in a HashMap<String, Object>.  That result hashmap has a number of entries in it:

  • status, which tells Robot is the test passed or failed
  • error, which identifies the error (if any)
  • traceback, which provides more context for the error
  • return, which is our general purpose object for sending back any data we wish to send

In this example, we are setting the return entry to contain “pong ” plus whatever we got as input in the first parameter.  Now, in real life, you would obviously want to be a little more careful about checking that parameters were passed and about typecasting them, but this is an example 🙂

Let’s look at a slightly more interesting keyword.  This one allows us to check the state of a server.  Here is the code:

  public HashMap<String, Object> server_status_should_be(Object args[]) {
    // prepare the result object
    HashMap<String, Object> result = new HashMap<String, Object>();
    // actual logic of the keyword goes here
    // TODO: check input is correct arity
    InitialContext ctx;
    boolean found = false;
    try {
      ctx = new InitialContext();
      MBeanServer server = (MBeanServer)ctx.lookup("java:comp/env/jmx/domainRuntime");
      ObjectName[] serverRuntimes = (ObjectName[]) server.getAttribute(domainRuntime, "ServerRuntimes");
      for (int i = 0; i < serverRuntimes.length; i++) {
        String serverName = (String) server.getAttribute(serverRuntimes[i], "Name");
        String serverState = (String) server.getAttribute(serverRuntimes[i], "State");
        if (((String)args[0]).compareTo(serverName) == 0) {
          // correct server
          if (((String)args[1]).compareTo(serverState) == 0 ) {
            // correct state
            found = true;
            break;
          }
        }
      }
    } catch (Exception e) {
      result.put(STATUS, FAIL);
      result.put(ERROR, e.getMessage());
      result.put(TRACEBACK, e.getStackTrace());
      result.put(RETURN, EMPTY);
      return result;
    }
    // create the response
    result.put(STATUS, found ? PASS : FAIL);
    result.put(ERROR, EMPTY);
    result.put(TRACEBACK, EMPTY);
    result.put(RETURN, EMPTY);
    return result;
  }

You can see that the same basic protocol is implemented here.  The only different is in what we actually do in the business logic to come up with an answer.  In this case, we look up the domainRuntime JMX MBeanServer, then go look for a server with the same name as the first parameter, and if we find it, we check if it’s state matches the second parameter.  The we return a result based on what we found.  Again, we should be more careful about our inputs existence and types, but you get the idea.

There is another keyword implemented that lets us check the state of an application.  The data source keyword is not implemented, it is sitting there with just the protocol wrapper, but no logic.  I thought you might like to have a go at implementing it 🙂

Following the keyword implementations, there are two more methods that we need to implement the protocol.  Oh by the way, we are not implementing the full protocol here – just the basics.  Let’s take a look at the get_keyword_names method:

  public String[] get_keyword_names() {
    // TODO: change this to use reflection?
    return new String[] { "ping",
                          "server_status_should_be",
                          "application_status_should_be",
                          "data_source_should_exist"
                        };
  }

Note that I am using the Python method naming standards here, since Robot is a Python-based framework.  This method just needs to return an array with the name of the keywords that are implemented.  I am doing this the lazy way – it would probably be better to produce the list with some kind of introspection, so you don’t forget to update it.

Finally, we need to implement the run_keyword method, which just routes the request to the right method to handle the keyword.  Again, I have implemented this the lazy way, and there are certainly better ways to do it. Here is the code:

  public HashMap<String, Object> run_keyword(String keyword, Object[] args) {
    HashMap<String, Object> kr = new HashMap<String, Object>();
    try {
      // run the right method
      // TODO: change this to use reflection perhaps?
      if (keyword.equalsIgnoreCase("ping")) {
        kr = ping(args);
      } else if (keyword.equalsIgnoreCase("server_status_should_be")) {
        kr = server_status_should_be(args);
      } else if (keyword.equalsIgnoreCase("application_status_should_be")) {
        kr = application_status_should_be(args);
      } else if (keyword.equalsIgnoreCase("data_source_should_exist")) {
        kr = data_source_should_exist(args);
      } else {
        kr.put(STATUS, FAIL);
        kr.put(RETURN, EMPTY);
        kr.put(ERROR, EMPTY);
        kr.put(TRACEBACK, EMPTY);
      }
    } catch (Exception e) {
      e.printStackTrace(System.out);
    }
    return kr;
  }

So that completes our project, let’s go ahead and test it out.  You can do that by just running

mvn verify

You should get some output like this:


C:\src\ci4fmw~robot\oracle-robot>mvn verify
 [INFO] Scanning for projects...
 [INFO]
 [INFO] ------------------------------------------------------------------------
 [INFO] Building oracleRobot 1.0-SNAPSHOT
 [INFO] ------------------------------------------------------------------------
 [INFO]
 [INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ oracle-robot ---
 [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
 [INFO] skip non existing resourceDirectory C:\src\ci4fmw~robot\oracle-robot\src\main\resources
 [INFO]
 [INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ oracle-robot ---
 [INFO] Nothing to compile - all classes are up to date
 [INFO]
 [INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ oracle-robot ---
 [WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
 [INFO] skip non existing resourceDirectory C:\src\ci4fmw~robot\oracle-robot\src\test\resources
 [INFO]
 [INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ oracle-robot ---
 [INFO] No sources to compile
 [INFO]
 [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ oracle-robot ---
 [INFO] No tests to run.
 [INFO]
 [INFO] --- maven-war-plugin:2.1.1:war (default-war) @ oracle-robot ---
 [INFO] Packaging webapp
 [INFO] Assembling webapp [oracle-robot] in [C:\src\ci4fmw~robot\oracle-robot\target\oracleRobot]
 [INFO] Processing war project
 [INFO] Copying webapp resources [C:\src\ci4fmw~robot\oracle-robot\src\main\webapp]
 [INFO] Webapp assembled in [171 msecs]
 [INFO] Building war: C:\src\ci4fmw~robot\oracle-robot\target\oracleRobot.war
 [WARNING] Warning: selected war files include a WEB-INF/web.xml which will be ignored
 (webxml attribute is missing from war task, or ignoreWebxml attribute is specified as 'true')
 [INFO]
 [INFO] --- weblogic-maven-plugin:12.1.2-0-0:deploy (wls-deploy) @ oracle-robot ---
 [INFO] Command flags are: -noexit -adminurl t3://localhost:7001 -deploy -user weblogic -password ******* -name oracleRobot -source C:\src\ci4fmw~robot\oracle-robot\target\oracleRobot.war -targets AdminServer -stage -upload -verbose
 weblogic.Deployer invoked with options:  -noexit -adminurl t3://localhost:7001 -deploy -user weblogic -name oracleRobot -source C:\src\ci4fmw~robot\oracle-robot\target\oracleRobot.war -targets AdminServer -stage -upload -verbose
 <22/10/2013 7:40:08 AM EST> <Info> <J2EE Deployment SPI> <BEA-260121> <Initiating deploy operation for application, oracleRobot [archive: C:\src\ci4fmw~robot\oracle-robot\target\oracleRobot.war], to AdminServer .>
 Task 3 initiated: [Deployer:149026]deploy application oracleRobot on AdminServer.
 Task 3 completed: [Deployer:149026]deploy application oracleRobot on AdminServer.
 Target state: deploy completed on Server AdminServer

Target Assignments:
 + oracleRobot  AdminServer
 [INFO]
 [INFO] --- robotframework-maven-plugin:1.1:run (default) @ oracle-robot ---
 ==============================================================================
 Acceptance
 ==============================================================================
 Acceptance.Mytest
 ==============================================================================
 Ping Test                                                             | PASS |
 ------------------------------------------------------------------------------
 Server State Test                                                     | PASS |
 ------------------------------------------------------------------------------
 Application State Test                                                | PASS |
 ------------------------------------------------------------------------------
 Data Source Existence Test                                            | PASS |
 ------------------------------------------------------------------------------
 Acceptance.Mytest                                                     | PASS |
 4 critical tests, 4 passed, 0 failed
 4 tests total, 4 passed, 0 failed
 ==============================================================================
 Acceptance                                                            | PASS |
 4 critical tests, 4 passed, 0 failed
 4 tests total, 4 passed, 0 failed
 ==============================================================================
 Output:  C:\src\ci4fmw~robot\oracle-robot\target\robotframework-reports\output.xml
 XUnit:   C:\src\ci4fmw~robot\oracle-robot\target\robotframework-reports\TEST-acceptance.xml
 Log:     C:\src\ci4fmw~robot\oracle-robot\target\robotframework-reports\log.html
 Report:  C:\src\ci4fmw~robot\oracle-robot\target\robotframework-reports\report.html
 [INFO] ------------------------------------------------------------------------
 [INFO] BUILD SUCCESS
 [INFO] ------------------------------------------------------------------------
 [INFO] Total time: 26.213s
 [INFO] Finished at: Tue Oct 22 07:40:23 EST 2013
 [INFO] Final Memory: 39M/776M
 [INFO] ------------------------------------------------------------------------
 C:\src\ci4fmw~robot\oracle-robot>

So we can see that it all worked.  And now we have a simple Robot Remote Library that we can extend as needed, and we can test it with Robot too.  Enjoy!

About Mark Nelson

Mark Nelson is an Architect (an "IC6") in the Fusion Middleware Central Development Team at Oracle. Mark's job is to make Fusion Middleware easy to use in the cloud and at home, for developers and operations folks, with special focus on continuous delivery, configuration management and provisioning - making it simple to manage the configuration of complex environments and applications built with Oracle Database, Fusion Middleware and Fusion Applications, on-premise and in the cloud. Before joining this team, Mark was a senior member of the A-Team since 2010, and worked in Sales Consulting at Oracle since 2006 and various roles at IBM since 1994.
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

2 Responses to Writing a Robot Remote Library

  1. autumnator says:

    Nice writeup. Is this writeup about a personal project or does it mean Oracle or IBM, etc. uses (or is evaluating) Robot Framework?

    • Mark Nelson says:

      Thanks, I cannot speak for Oracle “as a whole” or IBM at all, but I can say that there are some people in Oracle who do use Robot, and who are exploring how it could be used to test applications built on top of our platform.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s