Thursday, July 15, 2010

Cucumber on RedBridge

With JRuby's RedBridge, Ruby applications get started from Java. For example Cucumber, does. Let's see how Cucumber runs in Java code.

Firstly, people use Cucumber like this:

jruby -S cucumber addition.feature

"cucumber" is installed in $JRUBY_HOME/bin directory and looks like a command. But "cucumber" is a Ruby script, so "cucumber" can be evaluated using JRuby Embed, RedBridge, API. How about "addition.feature"? Cucumber receives the feature name via ARGV constant. This means adding a file name to ARGV will work. From these analysis, I wrote CucumberRunner.

package evergreen;

import org.jruby.embed.LocalContextScope;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class CucumberRunner {
private String jrubyhome = "/Users/yoko/Projects/jruby";
private String cucumber = jrubyhome + "/bin/cucumber";
private String feature = jrubyhome + "/lib/ruby/gems/1.8/gems/cucumber-0.8.5/examples/i18n/en/features/addition.feature";

private CucumberRunner() {
ScriptingContainer container = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
container.setHomeDirectory(jrubyhome);
container.runScriptlet("ARGV << '" + feature + "'");
container.runScriptlet(PathType.ABSOLUTE, cucumber);
}

public static void main(String[] args) {
new CucumberRunner();
}
}

This printed out:

# language: en
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers

Scenario Outline: Add two numbers # /Users/yoko/Projects/jruby/lib/ruby/gems/1.8/gems/cucumber-0.8.5/examples/i18n/en/features/addition.feature:7
Given I have entered <input_1> into the calculator # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:14
And I have entered <input_2> into the calculator # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:14
When I press <button> # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:18
Then the result should be <output> on the screen # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:22

Examples:
| input_1 | input_2 | button | output |
| 20 | 30 | add | 50 |
| 2 | 5 | add | 7 |
| 0 | 40 | add | 40 |

3 scenarios (3 passed)
12 steps (12 passed)
0m0.179s
/Users/yoko/Projects/jruby/lib/ruby/gems/1.8/gems/cucumber-0.8.5/bin/cucumber:19:in `load': exit (SystemExit)
from /Users/yoko/Projects/jruby/bin/cucumber:19
Exception in thread "main" org.jruby.embed.EvalFailedException: exit
at org.jruby.embed.internal.EmbedEvalUnitImpl.run(EmbedEvalUnitImpl.java:127)
at org.jruby.embed.ScriptingContainer.runUnit(ScriptingContainer.java:1149)
at org.jruby.embed.ScriptingContainer.runScriptlet(ScriptingContainer.java:1194)
at evergreen.CucumberRunner.(CucumberRunner.java:16)
at evergreen.CucumberRunner.main(CucumberRunner.java:20)
Caused by: org.jruby.exceptions.RaiseException: exit
at (unknown).(unknown)(/Users/yoko/Projects/jruby/lib/ruby/gems/1.8/gems/cucumber-0.8.5/bin/cucumber:19)
at Kernel.load(/Users/yoko/Projects/jruby/bin/cucumber:19)
at (unknown).(unknown)(:1)

OK. Cucumber seems to execute SystemExit in the end. Java code isn't happy with this sort of exception since evaluation itself has been succeeded. So, I'm going to catch org.jruby.embed.EmbedEvalFailedException and error message raised in Ruby.

package evergreen;

import java.io.StringWriter;

import org.jruby.embed.EvalFailedException;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class CucumberRunner1 {
private String jrubyhome = "/Users/yoko/Projects/jruby";
private String cucumber = jrubyhome + "/bin/cucumber";
private String feature = jrubyhome + "/lib/ruby/gems/1.8/gems/cucumber-0.8.5/examples/i18n/en/features/addition.feature";

private CucumberRunner1() {
StringWriter errorWriter = new StringWriter();
try {
ScriptingContainer container = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
container.setError(errorWriter);
container.setHomeDirectory(jrubyhome);
container.runScriptlet("ARGV << '" + feature + "'");
container.runScriptlet(PathType.ABSOLUTE, cucumber);
} catch (EvalFailedException e) {
System.out.println("Cuke says: " + e.getMessage());
} finally {
System.out.println("Cuke also says: " + errorWriter.toString());
}
}

public static void main(String[] args) {
new CucumberRunner1();
}
}

All right, CuumberRunner1 printed out:

# language: en
Feature: Addition
In order to avoid silly mistakes
As a math idiot
I want to be told the sum of two numbers

Scenario Outline: Add two numbers # /Users/yoko/Projects/jruby/lib/ruby/gems/1.8/gems/cucumber-0.8.5/examples/i18n/en/features/addition.feature:7
Given I have entered <input_1> into the calculator # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:14
And I have entered <input_2> into the calculator # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:14
When I press <button> # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:18
Then the result should be <output> on the screen # cucumber-0.8.5/examples/i18n/en/features/step_definitons/calculator_steps.rb:22

Examples:
| input_1 | input_2 | button | output |
| 20 | 30 | add | 50 |
| 2 | 5 | add | 7 |
| 0 | 40 | add | 40 |

3 scenarios (3 passed)
12 steps (12 passed)
0m0.238s
Cuke says: exit
Cuke also says: /Users/yoko/Projects/jruby/lib/ruby/gems/1.8/gems/cucumber-0.8.5/bin/cucumber:19:in `load': exit (SystemExit)
from /Users/yoko/Projects/jruby/bin/cucumber:19

Now, everything is controllable from Java.

Then, what if Cucumber result is showed up in Swing window? Isn't it interesting? So, I set StringWriter to ScriptingContainer so that all standard output from Ruby will be caught in StringWriter. Then, I added small Swing code.

package evergreen;

import java.io.StringWriter;

import javax.swing.JFrame;
import javax.swing.JTextArea;

import org.jruby.embed.EvalFailedException;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class CucumberRunner2 {
private String jrubyhome = "/Users/yoko/Projects/jruby";
private String cucumber = jrubyhome + "/bin/cucumber";
private String feature = jrubyhome + "/lib/ruby/gems/1.8/gems/cucumber-0.8.5/examples/i18n/en/features/addition.feature";

private CucumberRunner2() {
StringWriter writer = new StringWriter();
StringWriter errorWriter = new StringWriter();
try {
ScriptingContainer container = new ScriptingContainer(LocalContextScope.SINGLETHREAD);
container.setError(errorWriter);
container.setHomeDirectory(jrubyhome);
container.setWriter(writer);
container.runScriptlet("ARGV << '" + feature + "'");
container.runScriptlet(PathType.ABSOLUTE, cucumber);
} catch (EvalFailedException e) {
System.out.println("Cuke says: " + e.getMessage());
} finally {
System.out.println("Cuke also says: " + errorWriter.toString());
writeOnWindow(writer.toString());
}
}

private void writeOnWindow(String message) {
JFrame frame = new JFrame();
JTextArea text = new JTextArea(message);
frame.add(text);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}

public static void main(String[] args) {
new CucumberRunner2();
}
}

When I ran above code, a simple Swing window showed up with the result of Cucumber test.

Cucumber is Swing friendly with RedBridge. NetBeans plugin for Cucumber might be possible. Or, other Java based plugins for Cucumber might be easily implemented with RedBrdige.

Monday, July 12, 2010

Clojure uses DataMapper

Clojure is one of the JVM languages. People know this fact well. Also, people know well JRuby is among JVM languages. On the JVM languages, many people have used Java APIs from Clojure or Ruby code. But, we can do more since the JVM languages are able to communicate each other. The communication is done over the API exposed to Java such as JSR223. This means libraries and tools for a particular language are available to use in other JVM languages. For example, Clojure can choose DataMapper gem to interact databases. Usage is not so complicated. How could I make it happen? Here's a small example.


1. Installation

Clojure and JRuby themselves don't need to be installed. Grab the archives and unzip them. JRuby has installers, so you can use those if you like. After setting the path to jruby command, install 3 DataMapper gems. In this example, I used Sqlite3 for my DBMS. This is why I installed the DataMapper adapter for Sqlite3.

jruby -S gem install --no-ri --no-rdoc dm-core dm-sqlite-adapter dm-migrations



2. Run Clojure with the classpath to jruby.jar

My JRuby resides in /Users/yoko/Tools/jruby-1.5.1, so the path to jruby.jar is /Users/yoko/Tools/jruby-1.5.1/lib/jruby.jar. I added one more path. It is a directory for Ruby code for DataMapper, and the path name is /Users/yoko/Works/Samples/datamapper.
Move to the directory where Clojure was expanded, and run Clojure as in below:

java -cp clojure.jar:/Users/yoko/Tools/jruby-1.5.1/lib/jruby.jar:/Users/yoko/Works/Samples/datamapper clojure.main



3. Setup ScriptingContainer

This example uses JRuby's embed core, RedBridge, since it is easier to use compared to JSR223. The first step is to instantiate ScriptingContainer and set it up so that gems can be loaded.

user=> (import '(org.jruby.embed ScriptingContainer PathType))
org.jruby.embed.PathType
user=> (def c (ScriptingContainer.))
#'user/c
user=> (. c setHomeDirectory "/Users/yoko/Tools/jruby-1.5.1")
nil

To verify the setting was correct, I printed out Ruby's $LOAD_PATH.

user=> (. c runScriptlet "p $LOAD_PATH")
["clojure.jar", "/Users/yoko/Tools/jruby-1.5.1/lib/jruby.jar", "/Users/yoko/Works/Samples/datamapper",
"/Users/yoko/Tools/jruby-1.5.1/lib/ruby/site_ruby/1.8",
"/Users/yoko/Tools/jruby-1.5.1/lib/ruby/site_ruby/shared", "/Users/yoko/Tools/jruby-1.5.1/lib/ruby/1.8", "."]
nil

The path seems OK. Let's go forward.


4. Load gems and connect to the database

The second step is to load gems.

user=> (. c runScriptlet "require 'rubygems'; require 'dm-core'; require 'dm-migrations'")
true


Then, setup DataMapper.

user=> (. c runScriptlet "DataMapper.setup(:default, 'sqlite::memory:')")
#<RubyObject #<DataMapper::Adapters::SqliteAdapter:0xa75865>>


Writing Ruby code in method argument is not very nice. Instead, I wrote Ruby code in the *.rb files and evaluated each file loaded from classpath, /Users/yoko/Works/Samples/datamapper.

user=> (. c runScriptlet PathType/CLASSPATH "category_def.rb")
#<RubyObject #<DataMapper::Model::DescendantSet:0x3b9617>>

See category_def.rb below:

# Definition of the Category model
class Category
include DataMapper::Resource

property :id, Serial
property :category, String
property :created_at, DateTime
end

# Migration
DataMapper.auto_migrate!

Since the Category table has been created, I added three new records:

user=> (. c runScriptlet PathType/CLASSPATH "categories.rb")
true

See categories.rb below:

# Create new records
c1 = Category.new
c1.category = 'Kitchen & Food'
c1.save

c2 = Category.new
c2.category = 'Bed & Bath'
c2.save

c3 = Category.new
c3.category = 'Dining'
c3.save

Let's see what were input to Sqlite3.

user=> (. c runScriptlet "p Category.all")
[#<Category @id=1 @category="Kitchen & Food" @created_at=nil>, #<Category
@id=2 @category="Bed & Bath" @created_at=nil>, #<Category @id=3 @category="Dining" @created_at=nil>]
nil



As in this example, DataMapper worked with Clojure! MySQL and PostgreSQL are available to use from Clojure via DataMapper. Not just DataMapper. Other Ruby gems are also friends of Clojure.