Using Ruby to Automate Windows GUI Applications for Testing

Presenting Win32-autogui. A Ruby Win32 GUI testing framework packaged as a RubyGem.

Overview

Win32-autogui provides a framework to enable GUI application testing with Ruby. This facilitates integration testing of Windows binaries using Ruby based tools like RSpec and Cucumber regardless of the language used to create the binaries.

The source code repository is available here: http://github.com/robertwahler/win32-autogui. The repository contains specs and an example Win32 program with source and specs written in Delphi (Object Pascal).

Driving the Window's Calculator Application with IRB

Here is a quick demo using the Ruby Interactive Shell (IRB) under Cygwin on Windows XP to drive "calc.exe."

Install the Gem

Win32-autogui is available on RubyGems.org

gem install win32-autogui

IRB Session

Start up IRB

irb

Paste the following lines into your shell's IRB session.

Note: Window's "calc.exe" is used as the target binary by Win32-autogui's internal specs. The complete source to the wrapper is available here: spec/applications/calculator.rb.

    require 'win32/autogui'
    include Autogui::Input

    class Calculator < Autogui::Application
      def initialize
        super :name => "calc", :title => "Calculator"
      end
      def edit_window
        main_window.children.find {|w| w.window_class == 'Edit'}
      end
    end

Now we can start up the calculator

calc = Calculator.new
calc.running?

Session screenshot

Driving the calculator in IRB (1)

Driving the calculator in IRB (1)

Get some information

calc.pid
calc.main_window.window_class
calc.main_window.children.count

Perform a calculation

calc.set_focus; type_in('2+2=')

Get the result

calc.edit_window.text

Shut it down

calc.close
calc.running?

Session screenshot

Driving the calculator in IRB (2)

Driving the calculator in IRB (2)

RSpec + Win32-autogui for Testable GUI Specifications

The Win32-autogui repository contains an example Win32 program with source, testable binary, and specs written in Delphi (Object Pascal) located here: http://github.com/robertwahler/win32-autogui/tree/master/examples/quicknote.

Quicknote is a bare bones notepad clone. Here is the spec file spec/form_splash_spec.rb for the splash screen functionality.

    require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

    include Autogui::Input

    describe "FormSplash" do
      after(:all) do
        if @application.running?
          @application.splash.wait_for_close if @application.splash
          @application.file_exit 
          # still running? force it to close
          @application.close(:wait_for_close => true)
          @application.should_not be_running
        end
      end

      describe "startup with no command line parameters" do
        before(:all) do
          # --nosplash is the default, turn it back on
          @application = Quicknote.new :parameters => ''
          @application.should be_running
        end

        it "should show" do
          @application.splash.should_not be_nil
        end
        it "should close within 5 seconds" do
          @application.splash.should_not be_nil
          seconds = 5
          timeout(seconds) do
            @application.splash.wait_for_close
          end
          @application.splash.should be_nil
        end
      end

      describe "startup with '--nosplash' command line parameter" do
        it "should not show" do
          @application = Quicknote.new :parameters => '--nosplash'
          @application.should be_running
          @application.splash.should be_nil
        end
      end

    end

The Quicknote.exe application wrapper. Each of the testable application windows must be defined in a subclass of Autogui::Application. Partial code from lib/quicknote.rb.

    class Quicknote < Autogui::Application

      def initialize(options = {})
        # relative path to app using Windows style path
        @name ="exe\\quicknote.exe"  
        defaults = {
                     :title=> "QuickNote -", 
                     :parameters => '--nosplash', 
                     :main_window_timeout => 20
                   }
        super defaults.merge(options)
      end

      def edit_window
        main_window.children.find {|w| w.window_class == 'TMemo'}
      end

      def status_bar
        main_window.children.find {|w| w.window_class == 'TStatusBar'}
      end

      def dialog_about
        Autogui::EnumerateDesktopWindows.new.find do |w| 
          w.title.match(/About QuickNote/) && (w.pid == pid)
        end
      end

      def splash
        Autogui::EnumerateDesktopWindows.new.find do |w| 
          w.title.match(/FormSplash/) && (w.pid == pid)
        end
      end

      def message_dialog_confirm
        Autogui::EnumerateDesktopWindows.new.find do |w| 
          w.title.match(/Confirm/) && (w.pid == pid)
        end
      end

      # Title and class are the same as dialog_overwrite_confirm
      # Use child windows to differentiate
      def dialog_overwrite_confirm
        Autogui::EnumerateDesktopWindows.new.find do |w| 
          w.title.match(/^Text File Save$/) && 
            (w.pid == pid) && 
            (w.window_class == "#32770") &&
            (w.combined_text.match(/already exists/))
        end
      end

      # Title and class are the same as dialog_overwrite_confirm
      def file_save_as_dialog
        Autogui::EnumerateDesktopWindows.new.find do |w| 
          w.title.match(/Text File Save/) && 
            (w.pid == pid) &&
            (w.window_class == "#32770") &&
            (w.combined_text.match(/Save \&in:/))
        end
      end

      ...

Autotesting Quicknote with Watchr

Watchr provides a flexible alternative to Autotest.

NOTE: The following assumes a global setting of 'git config core.autocrlf input' and that you want to modify the Delphi 7 source to Quicknote which requires CRLF line endings.

Grab the source for Quicknote

cd ~/workspace
git clone http://github.com/robertwahler/win32-autogui -n
cd win32-autogui
git config core.autocrlf true
git checkout

Install watchr

gem install watchr

Run watchr

watchr spec/watchr.rb

Watchr will now watch the files defined in 'spec/watchr.rb' and run RSpec or Cucumber, as appropriate.

Session screenshot

Watchr session running form_splash_spec.rb

Watchr session running form_splash_spec.rb

For more information, please consult the source: http://github.com/robertwahler/win32-autogui.

comments

Using Git to Maintain Common RubyGem Functionality with BasicGem

Do you maintain several different RubyGems? Maybe you maintain dozens? Wouldn't it be nice to not have to repeat yourself when making changes that should be common to all the gems in your stable? For example, you decide that going forward, you will use Bundler for all your gem dependency needs. You could tweak your gemspecs and Rakefiles for each of your gems individually or you could use your customized fork of BasicGem as a common ancestor for all your gems. Now you can modify your BasicGem fork and merge these tweaks using Git into all your gems. As simple as...

cd ~/workspace/my_gem_cloned_from_my_basic_gem_fork
git pull my_basic_gem_fork HEAD
git mergetool

Introducing BasicGem, Gem Maintenance with Git

BasicGem is an opinionated RubyGem structure. BasicGem provides no stand-alone functionality. Its purpose is to provide a repository for jump-starting a new RubyGem and to provide a repository for cloned applications to pull future enhancements and fixes.

Features/Dependencies

Example Usage, Jump-starting a New Gem with BasicGem

The following steps illustrate creating a new gem called "mutagem" that handles file based mutexes. See http://github.com/robertwahler/mutagem for full source.

NOTE: We are cloning from BasicGem directly. Normally, you will want to clone from your own fork of BasicGem so that you can control and fine-tune which future BasicGem modifications you will support.

cd ~/workspace
git clone git://github.com/robertwahler/basic_gem.git mutagem
cd mutagem

Setup the repository for the cloned project

We are going to change the origin URL to our own server and setup a remote for pulling in future BasicGem changes. If our own repo for your new gem is setup at git@red:mutagem.git, change the URL with sed:

sed -i 's/url =.*\.git$/url = git@red:mutagem.git/' .git/config

Push up the unchanged BasicGem repo

git push origin master:refs/heads/master

Allow Gemlock.lock to be stored in the repo

sed -i '/Gemfile\.lock$/d' .gitignore

Add BasicGem (or your fork of BasicGem) as remote for future merges

git remote add basic_gem git://github.com/robertwahler/basic_gem.git

Rename your gem

Change the name of the gem from basic_gem to mutagem. Note that renames will be tracked in future merges since Git is tracking content and the content is non-trivial.

git mv lib/basic_gem.rb lib/mutagem.rb
git mv basic_gem.gemspec mutagem.gemspec

# commit renames now 
git commit -m "rename basic_gem files"

# BasicGem => Mutagem
find . -name *.rb -exec sed -i 's/BasicGem/Mutagem/' '{}' +
find . -name *.feature -exec sed -i 's/BasicGem/Mutagem/' '{}' +
sed -i 's/BasicGem/Mutagem/' Rakefile
sed -i 's/BasicGem/Mutagem/' mutagem.gemspec

# basic_gem => mutagem
find ./spec -type f -exec sed -i 's/basic_gem/mutagem/' '{}' +
find . -name *.rb -exec sed -i 's/basic_gem/mutagem/' '{}' +
find . -name *.feature -exec sed -i 's/basic_gem/mutagem/' '{}' +
sed -i 's/basic_gem/mutagem/' Rakefile
sed -i 's/basic_gem/mutagem/' mutagem.gemspec

Replace TODO's and update documentation

  • Replace README.markdown
  • Replace HISTORY.markdown
  • Replace TODO.markdown
  • Replace LICENSE
  • Replace VERSION
  • Modify .gemspec, add author information and replace the TODO's

Your gem should now be functional

rake spec
rake features

Setup git copy-merge

When we merge future BasicGem changes to our new gem, we want to always ignore some upstream documentation file changes.

Set the merge type for the files we want to ignore in .git/info/attributes. You could specify .gitattributes instead of .git/info/attributes but then if your new gem is forked, your forked repos will miss out on document merges.

echo "README.markdown merge=keep_local_copy" >> .git/info/attributes
echo "HISTORY.markdown merge=keep_local_copy" >> .git/info/attributes
echo "TODO.markdown merge=keep_local_copy" >> .git/info/attributes
echo "LICENSE merge=keep_local_copy" >> .git/info/attributes
echo "VERSION merge=keep_local_copy" >> .git/info/attributes

Setup the copy-merge driver. The "trick" is that the driver, keep_local_copy, is using the shell command "true" to return exit code 0. Basically, the files marked with the keep_local_copy merge type will always ignore upstream changes if a merge conflict occurs.

git config merge.keep_local_copy.name "always keep the local copy during merge"
git config merge.keep_local_copy.driver "true"

Commit

git add Gemfile.lock
git commit -a -m "renamed basic_gem to mutagem"

Add code to project's namespace

mkdir lib/mutagem
vim lib/mutagem/mutex.rb

Merging Future BasicGem Changes

Cherry picking method

git fetch basic_gem
git cherry-pick a0f9745

Merge 2-step method

git fetch basic_gem
git merge basic_gem/master

Trusting pull of HEAD

git pull basic_gem HEAD

Conflict resolution

NOTE: Most conflicts can be resolved with 'git mergetool' but 'CONFLICT (delete/modify)' will need to be resolved by hand.

git mergetool
git commit

BasicGem Provided Rake Tasks

rake -T

rake build         # Build mutagem-0.0.1.gem into the pkg directory
rake doc:clean     # Remove generated documenation
rake doc:generate  # Generate YARD Documentation
rake features      # Run Cucumber features
rake install       # Build and install mutagem-0.0.1.gem into system gems
rake release       # Create tag v0.0.1 and build and push mutagem-0.0.1.gem to Rubygems
rake spec          # Run specs
rake test          # Run specs and features

Autotesting with Watchr

Watchr provides a flexible alternative to Autotest. A jump start script is provided in spec/watchr.rb.

Install watchr

gem install watchr

Run watchr

watchr spec/watchr.rb

outputs a menu

Ctrl-\ for menu, Ctrl-C to quit

Watchr will now watch the files defined in 'spec/watchr.rb' and run Rspec or Cucumber, as appropriate. The watchr script provides a simple menu.

Ctrl-\

MENU: a = all , f = features  s = specs, l = last feature (none), q = quit
comments

Compiling EncFS for Ubuntu 8.04 LTS (Hardy Heron)

The Task

You are doing user-space filesystem encryption. You want to use a more recent version of EncFS than the one provided in the Ubuntu 8.04 repositories. No problem, just compile one yourself.

The Problem

The most recent version in the EncFS will not compile on Ubuntu 8.04. Version r53 12/7/09 configure.ac breaks with:

checking whether xattr interface takes additional options... no
./configure: line 24466: syntax error near unexpected token 'newline'

This issue has been reported to the EncFS maintainer, in the interim, you can compile a fairly recent version by following the steps below.

Reference Links

Preparation

Get the build tools

sudo apt-get install build-essential autoconf automake1.9 libtool gettext \
                     cvs pkg-config

Verify the kernel has FUSE support

cat /proc/filesystems | grep fuse

you should see something like this:

nodev   fuse
        fuseblk
nodev   fusectl

Install EncFS dependencies

sudo apt-get install libboost-dev libboost-filesystem-dev \
                     libboost-serialization-dev libfuse-dev \
                     fuse-utils librlog-dev libssl-dev

Building

Build version SVN r50 (f97ae2780) by pulling down with git and checking out the most recent version that will compile on Ubuntu 8.04

    cd ~/src

    git-svn clone --no-metadata  http://encfs.googlecode.com/svn/trunk encfs
    cd encfs
    git checkout -b work_around_build f97ae2780

    autoreconf -if
    ./configure
    make
    sudo make prefix=/usr install

Done!

comments

Copyright 1999-2011, GearheadForHire, LLC iconGearheadForHire, LLC
Site design by GearheadForHire, LLC | v2.1.1