robot-remote-server

0.1.0-SNAPSHOT


Implementation of a RobotFramework remote server in Clojure

dependencies

org.clojure/clojure
1.2.0
ring/ring-jetty-adapter
0.3.6
necessary-evil
1.0.0-SNAPSHOT

dev dependencies

swank-clojure
1.3.0-SNAPSHOT
marginalia
0.5.0



(this space intentionally left almost blank)
 

RobotFramework XML-RPC Remote Server in Clojure

This XML-RPC server is designed to be used with RobotFramework (RF), to allow developers to write RF keywords in Clojure.

If you use Leiningen, just run lein run to use the example keyword library included in the robot-remote-server.keyword namespace.

Otherwise, (:use) the robot-remote-server.core namespace in your own namespace containing RF keywords and add (server-start! (init-handler)) to start the remote server.

You can pass a map of options to (server-start!) like you would to (run-jetty). To stop the server, use (server-stop!) or send :stop_remote_server via RPC.

Because RF sends requests to the /RPC2 path, that has been enforced for this server using the wrap-rpc middleware defined in this namespace.

(ns robot-remote-server.core
  (:require [necessary-evil.core :as xml-rpc]
            [clojure.string :as str])
  (:import org.mortbay.jetty.Server)
  (:use [robot-remote-server keyword]
        ring.adapter.jetty))
(defonce *robot-remote-server* (atom nil))

Given a namespace and a fn-name as string, return the function in that namespace by that name

(defn find-kw-fn
  [a-ns fn-name]
  (ns-resolve a-ns (symbol fn-name)))

Make it nicer for Clojure developers to write keywords; replace underscores with dashes

(defn clojurify-name
  [s]
  (str/replace s "_" "-"))

Ring middleware to limit server's response to the particular path that RobotFramework petitions

(defn wrap-rpc
  [handler]
  (fn [req]
    (when (= "/RPC2" (:uri req))
      (handler req))))

Get arguments for a given RF keyword function identified by the string kw-name and located in the a-ns namespace

(defn get-keyword-arguments*
  [a-ns kw-name]
  (let [clj-kw-name (clojurify-name kw-name)
        a-fn (find-kw-fn a-ns clj-kw-name)]
    (vec (map str (last (:arglists (meta a-fn)))))))

Get documentation string for a given RF keyword function identified by the string kw-name and located in the a-ns namespace

(defn get-keyword-documentation*
  [a-ns kw-name]
  (let [clj-kw-name (clojurify-name kw-name)
        a-fn (find-kw-fn a-ns clj-kw-name)]
    (:doc (meta a-fn))))

Get a list of RF keyword functions located in the a-ns namespace

Given a RF-formatted string representation of a Clojure function kw-name in the a-ns namespace called with args as a vector, evaluate the function

(defn get-keyword-names*
  [a-ns]
  (vec
   (map #(str/replace % "-" "_") ; RF expects underscores
        (remove #(or (re-find #"(\*|!)" %) (re-find #"^-" %)) ; non-keyword functions
                (map str
                     (map first (ns-publics a-ns)))))))
(defn run-keyword*
  [a-ns kw-name args]
  (let [result {:status "PASS", ; RF expects this map
                :return "",
                :output "",
                :error "",
                :traceback ""}
        clj-kw-name (clojurify-name kw-name) ; translate RF keyword to Clojure fn
        a-fn (find-kw-fn a-ns clj-kw-name)
        output (with-out-str (try
                                (apply a-fn args)
                                (catch Exception e
                                  (assoc result
                                    :status "FAIL"
                                    :error (with-out-str (prn e))
                                    :traceback (with-out-str (.printStackTrace e))))))]
    (assoc result :output output :return output)))

WARNING: Less-than-functional code follows

Use of *robot-remote-server* inside the init-handler macro and in the two functions that follow. This has been done so that the XML-RPC server can offer the stop_remote_server command if desired.

Create handler for XML-RPC server. Set expose-stop to false to prevent exposing the stop_remote_server RPC command. Justification for using macro: delayed evaluation of *ns*

(defmacro init-handler
  [expose-stop]
  (let [this-ns *ns*]
    (if (true? expose-stop)
      `(->
        (xml-rpc/end-point
         {:get_keyword_arguments       (fn [kw-name#]
                                         (get-keyword-arguments* ~this-ns kw-name#))
          :get_keyword_documentation   (fn [kw-name#]
                                         (get-keyword-documentation* ~this-ns kw-name#))
          :get_keyword_names           (fn []
                                         (get-keyword-names* ~this-ns))
          :run_keyword                 (fn [kw-name# args#]
                                         (run-keyword* ~this-ns kw-name# args#))
          :stop_remote_server          (fn []
                                         (.stop @*robot-remote-server*))})
        wrap-rpc)
      `(->
        (xml-rpc/end-point
         {:get_keyword_arguments       (fn [kw-name#]
                                         (get-keyword-arguments* ~this-ns kw-name#))
          :get_keyword_documentation   (fn [kw-name#]
                                         (get-keyword-documentation* ~this-ns kw-name#))
          :get_keyword_names           (fn []
                                         (get-keyword-names* ~this-ns))
          :run_keyword                 (fn [kw-name# args#]
                                         (run-keyword* ~this-ns kw-name# args#))})
        wrap-rpc))))

Given a Ring handler hndlr, start a Jetty server

(defn server-start!
  ([hndlr] (server-start! hndlr {:port 8270, :join? false}))
  ([hndlr opts]
     (when (and (not (nil? @*robot-remote-server*)) (.isRunning @*robot-remote-server*))
       (.stop @*robot-remote-server*))
     (reset! *robot-remote-server* (run-jetty hndlr opts))))

Stop the global Jetty server instance

(defn server-stop!
  []
  (.stop @*robot-remote-server*))
 
(ns robot-remote-server.keyword
  (:use robot-remote-server.core)
  (:import javax.swing.JOptionPane))

Documentation for myKeyword

(defn my-keyword
  [arg1 arg2]
  (println (str "My first keyword! Arg1: " arg1 ", Arg2: " arg2)))

Open a JOptionPane, just testing things

(defn open-dialog
  []
  (JOptionPane/showMessageDialog
    nil "Hello, Clojure World!" "Greeting"
    JOptionPane/INFORMATION_MESSAGE))
(defn -main
  []
  (do (use 'robot-remote-server.core)
      (server-start! (init-handler false))))
 
(ns robot-remote-server.util)

TODO: Extend ResponseElements protocol to deal with nil

(comment
  
  (defn- handle-return-val
    "Convert everything to RobotFramework-acceptable types. See implementations in other languages for examples"
    [ret]
    (condp class ret
      java.lang.String                           ret
      java.util.concurrent.atomic.AtomicInteger  ret
      java.util.concurrent.atomic.AtomicLong     ret
      java.math.BigDecimal                       ret
      java.math.BigInteger                       ret
      java.lang.Byte                             ret
      java.lang.Double                           ret
      java.lang.Float                            ret
      java.lang.Integer                          ret
      java.lang.Long                             ret
      java.lang.Short                            ret
      clojure.lang.PersistentVector              (map handle-return-val ret)
      clojure.lang.PersistentArrayMap            (into {}
                                                       (for [[k v] ret]
                                                         [(.toString k) (handle-return-val v)]))
      clojure.lang.PersistentTreeMap             (into {}
                                                       (for [[k v] ret]
                                                         [(.toString k) (handle-return-val v)]))
      :else ret))

  (defonce a-server (run-jetty #'app-handler {:port 8271 :join? false}))
  (doto (Thread. #(run-jetty #'app-handler {:port 8271})) .start)
)