Snapp Action

Monday, June 18, 2007

Setting up Ruby on Rails with Apache 2

This is a rough beginning, but mostly has my notes from my experience setting up Ruby on Rails and some of the gotchas I came across. This was several months ago and I am going off the notes I took from that time. Most of the components used have later versions which may or may not work in this configuration.

I am currently running Open SuSE 10, with Apache2 (prefork). I added Ruby 1.8.4, Ruby Gems 0.8.11, mod_fcgid 1.09, and fcgi (www.fastcgi.com).

I chose mod_fcgid over mod_fastcgi because I found the latter to be buggy and unworkable. Though the version I used was 1.09, the homepage (http://fastcgi.coremail.cn/download.htm) mentions that they are all the way up to 2.1. After un-tarballing mod_fcgid, I needed to change the INCLUDE value in the Makefile to point to my apache2 include directories, so in my case it became:
INCLUDES=-I /usr/include/apache2 -I /usr/include/apache2-prefork

Then just do the usual
./configure --prefix=/usr
sudo make
sudo make install

Afterwards, I had an issue with the path fcgid was using to write to for its sockets under the apache web user. As it didn't have permissions to the original path, the fix was to put
SocketPath /tmp/fcgidsock
into httpd.conf. For me I added mod_fcgid.conf to the /etc/apache2/conf.d directory (which is picked up automatically) with the following values:
AddHandler fcgid-script .fcgi
SocketPath /tmp/fcgidsock
IPCConnectTimeout 10
IPCCommTimeout 60

At the location of my rails app, I made sure to change the user and group to one that could be used by apache with proper permissions to execute the dispatch.fcgi file in the public rails app directory. So,
chgrp -R www /path/to/rails
chown -R apache /path/to/rails

or whatever your user and group names may be. Also,
chmod -R 755 /path/to/rails

is most likely needed.

Next, I added the apache directives to my vhost.conf file at /etc/apache2/vhosts.d/vhost.conf. Alias, Directory, Location, and RewriteBase paths need to all match up in order to work. Here is a small example:
NameVirtualHost *:80

<VirtualHost *:80>
ServerName myhost.mydomain.com
DocumentRoot "/srv/www/htdocs/myhost"

SetEnv RAILS_ENV production
Alias /testrails/test /srv/www/htdocs/myhost/testrails/test/public
LogLevel debug

<Location "/testrails/test">
RewriteEngine On
RewriteBase /testrails/test
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
</Location>

<Directory "/srv/www/htdocs/myhost/testrails/test">
Options Indexes FollowSymLinks ExecCGI
AllowOverride None
Order allow,deny
Allow from all
AddHandler fcgid-script .fcgi
ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
</Directory>

</VirtualHost>

Now, moving to the rails app directory. The database.yml file needs to have the socket file added that mysql will use. So for instance, my production section looks similar to the following:
production:
adapter: mysql
.... rest of database, login info
host: localhost
socket: /var/lib/mysql/mysql.sock

Even after all this, when I went to view it from the web, every once in a while I would get a random "lost connection to mysql server during query" error. It turned out that the Ruby MySQL bindings had some issues and by replacing them with more reliable mysql-ruby C bindings my problem was fixed. I installed them by running:
gem install mysql -- --with-mysql-include=/usr/include/mysql --with-mysql-lib=/usr/lib/mysql

See http://wiki.rubyonrails.com/rails/pages/MySql+Connection+Problems for additional details on this issue.

From there you should be good to go. Good luck! Let me know how this tutorial can be improved.

Friday, February 16, 2007

SCons Unit Testing (with CxxTest)

In working with SCons I found unit testing critical to verify modules of C++ code. What I want to do here is let others know the method I discovered that fit my criteria:

  • Lightweight. It needed to be lightweight so we could add tests easily. Nobody wants to add tests if it they are difficult to create.

  • Executed automatically with each build. They are executed with each build so bugs can be caught early and before checking in code.

  • Extensible. Some pieces of our code modify or depend on aspects of the system (registry, smbios, network). While unit testing the modules of code could test the "recipe", so to speak, I also wanted a way to "bake the cake" to make sure it actually ran in certain environments (in other words, system tests). I also had other ideas in mind. Basically, by being extensible I wanted to be able to extend unit tests into other areas later on.

I researched several different implementations (see http://www.scons.org/wiki/UnitTests for starters). I think what I ended up with was a union of some the best suggestions given there.

CxxTest

CxxTest is a slick project for doing unit tests that does not require a lot of framework code just to run simple tests. See http://cxxtest.sourceforge.net to download it, and http://cxxtest.sourceforge.net/guide.html for instructions on how to use it. Yes, I did look into CppUnit, and in fact it is used by other members of my team, but I found CxxTest to be superior in terms of simplicity. You can read the links for more information, but the short of it is that by using Perl/Python, it can generate framework code based on a simple unit test class in a header file. I have used JUnit and felt right at home with coding CxxTest unit tests.

One customization I did was to convert the CxxTest python script cxxtestgen into an executable. That way I could call it like any other SCons tool. I don't know that that was necessary, but it was simple enough using py2exe.

Source tree structure

My tree is structured in the following way:

trunk/
build/ <-- this contains all build tools, and they are checked into our source control
scons/
cxxtest/
apps/ <-- this has the different applications we build
lib/ <-- the library of code that we share is in here
scons/ <-- a place for all my scons tools
UnitTest.py
util/
String.h
String.cpp
... (more source files)
test/
_MainPlaceHolder.h <-- I will describe this later
TestString.h
... (more unit tests)
out/ <-- This is the directory we build to

Adding the UnitTest tool to SCons

I took advantage of the fact that CxxTests are header files, and added a custom Builder for the ".h" suffix. This way, I could add the ".h" files themselves to the Program() call and it would know how to generate the .cpp files and build them.

If you are wondering about the _MainPlaceHolder.h, CxxTest will generate the test framework around as many test classes as you want, but it needs to also generate the main() entry point into the unit test. Since I don't want to create individual unit test binaries for each unit test header, _MainPlaceHolder.h is an empty file simply used as a file to key off of and generate the main() method for all tests in the "test" directory. There are probably a number of ways to work around this (i.e. pick the first test in the group to be the one with "main()", or generate/delete a place holder file automatcially when generating the unit test cpp files), I simply took the simplest choice at the time.

Here is the code in UnitTest.py:

import os
import string
import sys
import re

import SCons.MyHelper
import SCons.Util
import SCons

from SCons.Script import *

## in order to add unit test function
from SCons.Script.SConscript import SConsEnvironment

# A Custom Glob method that operates on the source directories rather than the BuildDir.
# This is actually factored into a different file (SCons.MyHelper) in my own build environment, but I
# have included it here for reference.
def Glob(env, pattern):
path = string.replace(env.GetBuildPath('SConscript'), 'SConscript', '')

result = []
for i in glob.glob(path + pattern):
result.append(string.replace(i, path, ''))

return result

def UnitTest(env, testlib, **kw):
testdir = 'test' # default test dir
runtests = 1 # automatically run tests when built

if (kw.has_key('CPPPATH')):
kw['CPPPATH'] += ['#/build/cxxunit', '.', '#']
if (kw.has_key('TESTDIR')):
testdir = kw['TESTDIR']
if (kw.has_key('RUNTESTS')):
runtests = kw['RUNTESTS']

if (kw.has_key('LIBS')):
kw['LIBS'] += testlib
else:
kw['LIBS'] = testlib

# automatically get all the .h files from the 'test' directory
source = SCons.MyHelper.Glob(env, testdir+'/*.h')

foundMain = 0
for s in source:
if '_MainPlaceHolder.h' in s:
foundMain = 1
break
if foundMain == 0:
source += [testdir + '/_MainPlaceHolder.h']

# create the name of the test executable (i.e. "test_mystuff.exe" or "systest_mystuff.exe")
name, ext = os.path.splitext(testlib[0].name)
name = testdir + '_' + name
test = env.Program(name, source, **kw)

# run tests if user has not overridden their use from the command line with 'runtests=0'
if ( env['RUNTESTS'] == 1 ):
# run tests at the end, if runtests is true (this may be a system test, which is not meant to be run immediately)
if runtests:
env.AddPostAction(test, test[0].abspath);
env.Alias(testdir, test) # Alias to testdir which should be unique
env.AlwaysBuild(test)

return test

def build_unit_test(target, source, env):
for t, s in zip(target, source):
if (str(s).endswith('_MainPlaceHolder.h')):
env.Execute('cxxtestgen --error-printer --root -o %s %s' % (t, s), strfunction='')
else:
env.Execute('cxxtestgen --part -o %s %s' % (t, s), strfunction='')

return 0

def unit_test_string(target, source, env):
h, t = os.path.split(str(source[0]))
return "unit testing '%s'" % (h)

## regex to find unit test headers
header_re = re.compile(r"([^.]+)\.h$")

def unit_test_emitter(target, source, env):
target = [] # for some reason this is coming in with a prefilled value

for item in source:
dir, file = os.path.split(str(item))
match = header_re.search(file)
trunkdir, testdir = os.path.split(dir)
target += [testdir + '/' + match.group(1) + "_gen.cpp"]

return (target, source)

def generate(env):
"""
Add builder and UnitTest wrapper for unit testing
"""

unit_test_action = Action(build_unit_test, strfunction=unit_test_string)
bld = Builder(action = unit_test_action, emitter = unit_test_emitter, src_suffix = '.h') #, suffix = '.cpp')
env.Append(BUILDERS = {'Test' : bld})

# adding test builder to src builders
static_obj, shared_obj = SCons.Tool.createObjBuilders(env)
static_obj.src_builder.append('Test')
shared_obj.src_builder.append('Test')

SConsEnvironment.UnitTest = UnitTest

def exists(env):
"""
Make sure this tool exists.
"""
return env.Detect('cxxtestgen') # this is why I converted cxxtestgen.py into an executable

In my SConstruct, I simply added UnitTest to my Environment tools:
env = Environment(platform=target_info['build_os'],
tools=target_info['tools'] + ['UnitTest'] + ['msvs'] + ['VSProj'],
ENV=target_info['env'],
toolpath = ['.', 'lib/scons'])

Now, in order to perform unit tests, all we need is to pass the result of the build into UnitTest (in this case "lib"). Note that we don't need to specify the header files that contain unit tests since the builder will automatically gather them from the TESTDIR. Here is an example:
# define sources, includes, defines, dependant libraries and lib paths, etc.
...

# create the library or executable binary as usual
lib = env.StaticLibrary('util', sources, CPPPATH=includes, CPPDEFINES=defs)

# Build unit/system tests, unit tests run immediately
test = env.UnitTest(lib, CPPPATH=includes, CPPDEFINES=defs, LIBS=libs, LIBPATH=libpath) # TESTDIR is 'test' and RUNTESTS is 1 by default
systest = env.UnitTest(lib, CPPPATH=includes, CPPDEFINES=defs, LIBS=libs, LIBPATH=libpath, RUNTESTS=0, TESTDIR='systest')
Here is an example test case for lib/util/String that shows how simple the test cases can be:
#include <cxxtest/TestSuite.h>
#include "String.h"
using namespace std;

class TestString : public CxxTest::TestSuite
{
public:
void testString_1( void )
{
String str1 = "This IS a Test STRing";
String str2 = "THIS is a TEST strinG";

// test case insensitive comparisons
TS_ASSERT_EQUALS( str1, str2 );
}
}

That's all it takes to create a unit test. Pretty cool, huh?

Conclusion

By using a simple tool for building unit tests (CxxTest) and the SCons build tool, we can create a simple, but powerful way to test modules of code that requires minimal effort on the developer's part.

Feel free to respond with suggestions for improvement.