Practical Clojure with SWT, JUnit and Spring

Date: Mon Apr 06 13:56:55 EDT 2009



The following entry describes a practical approach for using the Clojure programming language. Clojure is a new lisp dialect that targets the Java Virtual Machine. It has a lot of syntatic sugar of other Lisp languages but can also be used interoperate with existing Java libraries. This entry describes how to build a simple test helper GUI application that launches a script to compile your tests, run your tests and also runs Java's hprof and verbose garbage collection statistics. I have always had trouble compiling, launching and manipulating a collection of test scripts. By using clojure and this tool, I can quickly launch of series of tests without having to visit the command line. In reality, this test only LAUNCHES a script that then launches the actual test main application. So, the GUI application only acts as a facility for launching other processes. In our case, the processes are the Clojure test suites. The hprof and verbosegc command-line arguments are used depending on the button even that is invoked. The application relies on SWT (Eclipse's Standard Widget Toolkit), Clojure and Spring. Spring is not really needed for this type of small applcation but I introduce it here so that you have an example on how to use the framework alongside Clojure.

Java oriented approach for Bootstrapping Clojure and Spring:

There are several different approaches for invoking Clojure on your source:

  • clojure.lang.Script - Use the Script main class to invoke Clojure on a particular source file.
  • clojure.lang.Repl - Use the Repl main class to invoke the Clojure Repl loop on a file
  • Compile Clojure source to a Java bytecode and then invoke that particular main class
  • Compile your own bootstrap code that uses the Clojure java API. We used this approach for bootstrapping the GUI application. Essentially, we are using the low-level Java Clojure code to invoke clojure.main.

The Java Bootstrap Code:


I wrote a small blog entry on how to call Clojure from Java using Clojure's low-level API. It boiled down to taking the code from clojure.lang.Script and reworking some of the calls. The goal is to invoke the Clojure runtime and launch our script.

private static final String BEAN_FACTORY = "beanFactoryRef-testwin.xml";
private static final String [] CLASSPATH_CONTEXTS = { "conf/applicationContext-testwin.xml" };
private static final String BASIC_TEST_WIN_GLOBALS = "light.test.win.spring_globals";
private static final String BASIC_TEST_WIN_NAMESPACE = "light.test.win.basic_test_window";

public Object invokeContract(Object precondInput) throws ContractError {
final ApplicationContext context = (ApplicationContext) precondInput;
//////////////////////////////////
// Init the clojure main library
//////////////////////////////////
final Symbol symbolClojureMain = Symbol.create("clojure.main");
final Namespace namespaceClojureMain = Namespace.findOrCreate(symbolClojureMain);
final Var varRequire = Var.intern(RT.CLOJURE_NS, Symbol.create("require"));

// Setup clojure/main
try {
varRequire.invoke(symbolClojureMain);

// Call require on our utility clojure code
// Set the variable spring-context for use in the clojure script
Var.intern(Namespace.findOrCreate(Symbol.create(BASIC_TEST_WIN_GLOBALS)), Symbol.create("*spring-context*"), context);

// Launch the main window.
varRequire.invoke(Symbol.create(BASIC_TEST_WIN_NAMESPACE));

} catch (Exception e) {
throw new ContractException(e.getMessage());
}

return CONTRACT_IGNORE;
}

Here is the main method. Before, calling the Clojure routines, we first load the Spring application context:

The purpose of the outer Spring BeanFactory is to load our inner ApplicationContext instances. This is the Spring related stuff that isn't needed to work with Clojure, but if you did need to work with your existing Java libraries, this is one approach to use.


import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.access.BeanFactoryReference;
import org.springframework.beans.factory.access.SingletonBeanFactoryLocator;
import org.springframework.context.ApplicationContext;

import clojure.lang.Namespace;
import clojure.lang.RT;
import clojure.lang.Symbol;
import clojure.lang.Var;

public static void main(final String [] args) throws Exception {
final BeanFactoryLocator bfl = SingletonBeanFactoryLocator.getInstance(BEAN_FACTORY);
final BeanFactoryReference bf = bfl.useBeanFactory("com.lightedit.clojure.LightApplicationContext");
// now use some bean from factory
final BeanFactory beanFactoryContext = bf.getFactory();
final IContractHandler contract = new BasicTestWinMain();
contract.executeContract(beanFactoryContext);
}

Spring and Clojure are loaded at the application start and we also set a Clojure variable with the Spring context as its value. This is so that we can access the Spring beans at any time within our Clojure code.

;; Java Code:
;; // Call require on our utility clojure code
;; // Set the variable spring-context for use in the clojure script
;; Var.intern(Namespace.findOrCreate(Symbol.create(BASIC_TEST_WIN_GLOBALS)), Symbol.create("*spring-context*"), context);
;;
;; Clojure Use of the Context Instance:
(. *spring-context* getBean "testWinProperties")

Basic Lisp Programming in Clojure:
If you haven't worked with a Lisp dialect then you may not be used to the simple syntax. But I can guarantee that the syntax is simple. Idiomatic programming with Clojure may not be simple and it may take a while to master but the syntax is simple. Here is the basic BNF form for lisp:

expression = '(' expression* ('.' expression)? ')' | SYMBOL | NUMBER

Basically, expressions exist between a matching left parenthesis and right parenthesis. Typically, the first token is the function or macro and the rest of the tokens are the arguments. In this example below, the arguments 1 2 3 are passed to the ADD function.

bbrown@houston:~$ clj
Clojure
user= (+ 1 2 3)
6
user=

Here is the syntax to define a function and then call the function with one argument.
user=> (defn abc [arg1] (println arg1))
#'user/abc
user= (abc "123")
123
nil

SWT (Standard Widget Toolkit) and Clojure

There are only thirty or forty lines of Java code and 700-800 lines of Clojure code (with source). The Clojure code is a simple SWT application that has a main text area and six buttons. Each button contains an event. On the event, a separate Java process is launched.

  • light/test/win/basic_constants.clj -- Global string and other constant definitions.
  • light/test/win/basic_gui_utils.clj -- SWT Gui utilities, open dialog boxes, etc.
  • light/test/win/basic_test_utils.clj -- Misc string, regex utilties.
  • light/test/win/basic_test_window.clj -- MAIN set of routines to launch the SWT window, build the six buttons and establish the event handlers.
  • light/test/win/global_objects.clj -- Global instances of the main SWT widgets (the shell, the display, etc)

basic_test_window.clj


The basic_test_window.clj Clojure source is where everything gets started for the SWT application. With the code below, even though the syntax is in Clojure, you would use a similar approach in Java to build the SWT window. Also, these are just code snippets, see the downloads below to get the full source.
            
(defn create-shell [disp sh]
;; Note change in 'doto' call, dot needed.
(let [layout (create-grid-layout)]
(doto sh
(. setText *Basic_Window_title*)
(. setLayout layout)
(. addShellListener (proxy [ShellAdapter] []
(shellClosed [evt] (exit)))))))

(defn create-gui-window
"Initialize the SWT window, set the size add all components"
[disp sh]
(init-gui-helper disp sh)
(let [gd (new GridData SWT/FILL SWT/FILL true false)]
(. search-box addListener SWT/Traverse find-text-listener)
(. search-box setLayoutData gd)
(. location-bar setLayoutData gd)
(. status-bar setLayoutData gd))
;; Final init, set the window size and then open
(doto sh
(. setSize win-size-width win-size-height)
(. open))
;; Debug
(loop [] (if (. sh (isDisposed))
(. disp (dispose))
(let [] (when (not (. disp (readAndDispatch)))
(. disp (sleep)))
(recur)))))

(defn main-1
" Application Entry Point, launch the main window and wait for events"
[]
;;;;;;;;;
(println "Launching Text Test Viewer...")
(create-gui-window *display* *shell*)
(let [o (new Object)] (locking o (. o (wait)))))

Button start-process listeners:


;; Listeners and Event Handlers for Compiling and Running the Tests
;; The event will spawn a script process that compiles the tests of interest

;; Invoke the compile process and log the output to the main window"
;; The following tests are available:
;; compile, runtests, singletest, singlemem, singlehprof
(defmacro def-start-process [test-type]
`(let [test-props-bean# (. *spring-context* getBean "testWinProperties")]
(println "Attempt to start process, single class =>" (.getSingleTestClass test-props-bean#))
(start-process [ *process-gentests-sh* ~test-type
(.getSingleTestClass test-props-bean#) ] buffer-1)))

(defmacro def-button-listener [test-type]
`(proxy [~'SelectionListener][]
(widgetSelected [event#] (def-start-process ~test-type))
(widgetDefaultSelected [event#] (def-start-process ~test-type))))

basic_test_window.clj - SWT Source for Invoking the Main Window

The JUnit Clojure Tests:




Here is a portion of the Win32 script/process to launch the JUnit tests. Just a reminder, the GUI tool's only function is to launch this script. The script then launches the JUnit Java process or compile process.

...
...
:compile
REM -- run suite for just a single test --
%_RUNJAVA% %JAVA_OPTS% -classpath "%CLASSPATH%" -Dlight.install.dir="%INSTALL_DIR%" clojure.lang.Script ...
goto end

:runtests
REM -- run suite --
%_RUNJAVA% %JAVA_OPTS% test.light_test_suite ...
goto end

:singletest
REM -- run suite --
%_RUNJAVA% %JAVA_OPTS% ... test.light_test_suite_single ...
goto end

:singlemem
REM -- run suite --
%_RUNJAVA% %JAVA_OPTS% -verbosegc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps test.light_test_suite_single ...
goto end

:singlehprof
REM -- run suite --
%_RUNJAVA% %JAVA_OPTS% -verbosegc -Xrunhprof:file=classes/hprof_dump.txt,format=a test.light_test_suite_single ...
goto end
:end
exit /b

There are two steps to work with JUnit, compile the JUnit test cases by using Clojure's compile function and then running the application as you would normally run Java bytecode. Compile and Run. This is the source for compile_tests.clj:
(defn main []
(println "Compiling Tests")
(compile 'test.light_sample_test)
(compile 'test.light_mergelogs_test)
(compile 'test.light_test_suite)
(compile 'test.light_test_suite_single)
(println "Done Compiling"))

(try (main) (catch Exception e (. e printStackTrace)))

(. System exit 1)

Here is the source snippet for light_sample_test.clj:. When the bytecode is built for this class, it will resemble a typical class built with the javac compiler:
;; Make sure that the 'classes' directory exists
(ns test.light_sample_test
(:import (junit.framework Assert))
(:gen-class
:methods [[testDog [] void]]
:extends junit.framework.TestCase))

(defn -init [_] ())

(defn -testDog [_]
(println "Welcome to Light")
(Assert/fail "Test not implemented"))

Application Usage:

Install: The best way to install and use the test GUI is to install all of the files into:

c:\usr\local\projects\testtoolkit (/usr/local/projects/testtoolkit on Linux) directory. For example, testtoolkit is LIGHT_HOME:

Z:\> cd "c:\usr\local\projects\testtoolkit"

Java Runtime 1.5 or greater is required.
WIN32:

In a win32 environment (Cygwin or through the Windows Command Line), the best way to launch Light is to execute the light.bat batch script.
Z:\> light_test.bat (or light_test.bat in cygwin)

Dependencies


Like a lot of Java/J2EE applications, this application requires a number of third party libraries. Spring, Clojure and SWT are the main ones. The libraries are already included in the download file, but here is a listing of the jar files and how they are used.

  • clojure.jar - Clojure programming language, full library (vers 200903)
  • octane_commons.jar - Jar file that includes the simple Contract binary and Bootstrap Clojure/Spring code. See the source listing below.
  • swt/linux/swt.jar - SWT jar for linux, Standard Widget Toolkit library.
  • junit-4.4.jar - Junit 4.4
  • spring/spring-custom.jar - Spring 2.5 library (contains only a subset of the spring libraries for use with this application)

Additional Source Code Modules


  • light_test.bat - Batch script for Win32 (Note: it is advised to use the hardcoded path, 'C:\usr\local\projects\testtoolkit')
  • light_test.sh - Batch script for Linux

  • build.xml -- Ant build script for building the Java bootstrap source
  • src/com/light/clojure/BasicTestWinMain.java -- Java source for bootstrapping Clojure and loading the Spring configuration
  • src/com/light/clojure/test/TestWinProperties.java -- Simple bean used with Spring, contains only one field. We use constructor injection to set the value.
  • src/com/light/contract/BasicContractHandler.java -- Simple design by contract oriented library
  • src/com/light/contract/IContractHandler.java -- Simple design by contract oriented library

Source Code

SVN Directory for Clojure GUI Application
example_clojure_swt.tar.gz - Source and Binary

References

clojure.org

[1] Clojure API Examples

[2] SVN Browsable Source for this Project

[3] http://java.sun.com/docs/hotspot/gc1.4.2/example.html- Diagnosing a Garbage Collection problem

[4] Most Comprehensive Java Perf Guide - http://www.javaperformancetuning.com/

[5] http://oreilly.com/catalog/javapt/chapter/ch04.html- More good performance tips.

[6] https://hat.dev.java.net/ - Heap analysis tool. This tool is included with JDK6.

[7] Java technology, IBM style: Garbage collection policies, Part 1

Contact me for additions to the entry or other comments:
Berlin Brown - berlin dot brown at gmail.com

Blog Entry Updates:
  • 4/4/2009 - Initial Blog Entry Version

----------

Comments

Popular posts from this blog

Is Java the new COBOL? Yes. What does that mean, exactly? (Part 1)

On Unit Testing, Java TDD for developers to write

JVM Notebook: Basic Clojure, Java and JVM Language performance