Tags: automation

RSS - Atom - Subscribe via email

Drush, Simpletest, and continuous integration for Drupal using Jenkins (previously Hudson)

Posted: - Modified: | drupal, geek

One of my development goals is to learn how to set up continuous integration so that I’ll always remember to run my automated tests. I picked up the inspiration to use Hudson from Stuart Robertson, with whom I had the pleasure of working on a Drupal project before he moved to BMO. He had set up continuous integration testing with Hudson and Selenium on another project he’d worked on, and they completed user acceptance testing without any defects. That’s pretty cool. =)

I’m a big fan of automated testing because I hate doing repetitive work. Automated tests also let me turn software development into a game, with clearly defined goalposts and a way to keep score. Automated tests can be a handy way of creating lots of data so that I can manually test a site set up the way I want it to be. I like doing test-driven development: write the test first, then write the code that passes it.

Testing was even better with Rails. I love the Cucumber testing framework because I could define high-level tests in English. The Drupal equivalent (Drucumber?) isn’t quite there yet. I could actually use Cucumber to test my Drupal site, but it would only be able to test the web interface, not the code, and I like to write unit tests in addition to integration tests. Still, some automated testing is better than no testing, and I’m comfortable creating Simpletest classes.

Jenkins (previously known as Hudson) is a continuous integration server that can build and test your application whenever you change the code. I set it up on my local development image by following Jenkins’ installation instructions. I enabled the Git plugin (Manage Jenkins – Manage Plugins – Available).

Then I set up a project with my local git repository. I started with a placeholder build step of Execute shell and pwd, just to see where I was. When I built the project, Hudson checked out my source code and ran the command. I then went into the Hudson workspace directory, configured my Drupal settings.php to use the database and URL I created for the integration site, and configured permissions and Apache with a name-based virtual host so that I could run web tests.

For build steps, I used Execute shell with the following settings:

mysql -u integration integration < sites/default/files/backup_migrate/scheduled/site-backup.mysql
/var/drush/drush test PopulateTestUsersTest
/var/drush/drush test PopulateTestSessionsTest
/var/drush/drush testre MyProjectName --error-on-fail

This loads the backup file created by Backup and Migrate, sets up my test content, and then uses my custom testre command.

Code below (c) 2011 Sacha Chua (sacha@sachachua.com), available under GNU General Public License v2.0 (yes, I should submit this as a patch, but there’s a bit of paperwork for direct contributions, and it’s easier to just get my manager’s OK to blog about something…)

// A Drush command callback.
function drush_simpletest_test_regular_expression($test_re='') {
  global $verbose, $color;
  $verbose = is_null(drush_get_option('detail')) ? FALSE : TRUE;
  $color = is_null(drush_get_option('color')) ? FALSE : TRUE;
  $error_on_fail = is_null(drush_get_option('error-on-fail')) ? FALSE : TRUE;
  if (!preg_match("/^\/.*\//", $test_re)) {
    $test_re = "/$test_re/";
  }
  // call this method rather than simpletest_test_get_all() in order to bypass internal cache
  $all_test_classes = simpletest_test_get_all_classes();

  // Check that the test class parameter has been set.
  if (empty($test_re)) {
    drush_print("\nAvailable test groups & classes");
    drush_print("-------------------------------");
    $current_group = '';
    foreach ($all_test_classes as $class => $details) {
      if (class_exists($class) && method_exists($class, 'getInfo')) {
        $info = call_user_func(array($class, 'getInfo'));
        if ($info['group'] != $current_group) {
          $current_group = $info['group'];
          drush_print('[' . $current_group . ']');
        }
        drush_print("\t" . $class . ' - ' . $info['name']);
      }
    }
    return;
  }

  // Find test classes that match
  foreach ($all_test_classes as $class => $details) {
    if (class_exists($class) && method_exists($class, 'getInfo')) {
      if (preg_match($test_re, $class)) {
        $info = call_user_func(array($class, 'getInfo'));
        $matching_classes[$class] = $info;
      }
    }
  }

  // Sort matching classes by weight
  uasort($matching_classes, '_simpletest_drush_compare_weight');

  foreach ($matching_classes as $class => $info) {
    $main_verbose = $verbose;
    $results[$class] = drush_simpletest_run_single_test($class, $error_on_fail);
    $verbose = $main_verbose;
  }

  $failures = $successes = 0;
  foreach ($results as $class => $status) {
    print $status . "\t" . $class . "\n";
    if ($status == 'fail') {
      $failures++;
    } else {
      $successes++;
    }
  }
  print "Failed: " . $failures . "/" . ($failures + $successes) . "\n";
  print "Succeeded: " . $successes . "/" . ($failures + $successes) . "\n";
  if ($failures > 0) {
    return 1;
  }
}

I didn’t bother hacking Simpletest output to match the Ant/JUnit output so that Jenkins could understand it better. I just wanted a pass/fail status, as I could always look at the results to find out which test failed.

What does it gain me over running the tests from the command-line? I like having the build history and being able to remember the last successful build.

I’m going to keep this as a local build server instead of setting up a remote continuous integration server on our public machine, because it involves installing quite a number of additional packages. Maybe the other developers might be inspired to set up something similar, though!

2011-06-09 Thu 09:51

Rails: Exporting data from specific tables into fixtures

Posted: - Modified: | geek, rails

Rails is pretty darn amazing. There are plenty of gems (Ruby packages) that provide additional functionality. They’re like Drupal modules, except with more customizability (not just hooks) and fewer pre-built administrative interfaces (you win some, you lose some).

For example, the client asked me, “Can we edit the static content?” Now if I had asked about this as a requirement at the beginning of the project, we might have gone with Drupal instead–although the Rails Surveyor still feels cleaner than a CCK-based survey type, so we might’ve stayed with Rails.

Anyway, we were well into Rails now, so I looked for a content management system that I could integrate into the Rails 3-based website. After some experimenting with Refinery CMS (looks slick, but couldn’t get it to do what I wanted) and Comfortable Mexican Sofa (looked pretty geeky), I settled on Rich CMS. I nearly gave up on Rich CMS, actually, because I’d gotten stuck, but the web demo helped me figure out what I needed to do in order to enable it.

We’re still emptying and reloading the database a lot, though, so I wanted to make sure that I could save the CmsContent items and reload them. I didn’t want to back up the entire database, just a table or two. There were some gems that promised the ability to back up specific models, but I couldn’t figure it out. Eventually I decided to use the table-focused Rake code I saw in order to export the data to fixtures (seems to be based on code from the Rails Recipes book).

task :extract_fixtures => :environment do
  sql  = "SELECT * FROM %s"
  skip_tables = ["schema_info"]
  ActiveRecord::Base.establish_connection
  if (not ENV['TABLES'])
    tables = ActiveRecord::Base.connection.tables - skip_tables
  else
    tables = ENV['TABLES'].split(/, */)
  end
  if (not ENV['OUTPUT_DIR'])
    output_dir="#{RAILS_ROOT}/test/fixtures"
  else
    output_dir = ENV['OUTPUT_DIR'].sub(/\/$/, '')
  end
  (tables).each do |table_name|
    i = "000"
    File.open("#{output_dir}/#{table_name}.yml", 'w') do |file|
      data = ActiveRecord::Base.connection.select_all(sql % table_name)
      file.write data.inject({}) { |hash, record|
        hash["#{table_name}_#{i.succ!}"] = record
        hash
      }.to_yaml
      puts "wrote #{table_name} to #{output_dir}/"
    end
  end
end

Being a lazy programmer who doesn’t want to remember table names, I also defined the following Rake tasks:

task :save_content => :environment do
  ENV["TABLES"] = "cms_contents"
  Rake.application.invoke_task("myproj:extract_fixtures")
end
task :load_content do
  Rake.application.invoke_task("db:fixtures:load")
end

Then I can call rake myproj:save_content and rake myproj:load_content to do the right thing. Or rather, my co-developer (a new IBMer – hello, Vijay!) can do so, and then check his work into our git repository. =)

Now we can re-create the development database as often as we’d like without losing our page content!

2011-04-24 Sun 16:29

Cucumber, Capybara, and the joys of integration testing in Rails

Posted: - Modified: | geek, rails

Development is so much more fun with test cases. They give you a big target to aim for, and it feels fantastic when you write the code to make them pass. Tests also avoid or shorten those late-night “oh no! I broke something!” sessions, because you can backtrack to versions that pass the tests. (You are using version control, right?)

So naturally, as I worked on my first IBM project using Ruby on Rails, I wanted to know about how to perform automated testing – not just at the unit level, but at the web/integration level.

I like using Simpletest in Drupal. I love the testing frameworks available in Rails.

You see, Cucumber for Rails allows you to write your tests in English (or something reasonably close to it). For example:

Feature: Contributor
  In order to maintain security
  As a contributor
  I want to be able to edit existing submissions
  Scenario: Contributor should not be able to create or delete submissions
    Given I am a company contributor
    And there is a 2010 survey for "Company X"
    When I view the dashboard
    Then I should not be able to delete a submission
    And I should not be able to create a submission

Putting that in my features/contributor.feature" file and executing that with =bundle execute cucumber features/contributor.feature gets me a lovely test with green signs all around.

You’re thinking: Rails is awesome, but it’s not that awesome, is it? How can it know about the specifics of the application?

Rails knows because I’ve written my own step definitions for Cucumber. Step definitions are simple. You can define them with a regular expression like this:

When /^I view the dashboard/ do
  visit root_path
end

Then /^I should not be able to create a submission/ do
  page.should_not have_button("Create submission")
end

You can also define steps that parse arguments from the string or call other steps:

Given /^there is a ([^ ]+) survey for \"([^\"]+)\"$/ do |year,name|
  @company = Company.find_by_name(name)
  assert !@company.nil?
  Given "there is a #{year} survey"
end

You can even take multi-line input, such as tables.

Automated testing is so awesome!

Helping kids learn about automation

Posted: - Modified: | teaching

J- shuffled in and out of the living room, listless and bored. As part of a 9-week simulation of real life in school, she and her classmates had been assigned jobs. Her job was to be an accountant, and the tedium of checking dozens of pretend tax returns had long sunk in. W- had encouraged her to use a calculator, so at least she didn’t have to multiple all those figures by hand, but there were still so many numbers to verify.

My geek sense tingled, as it does whenever there’s an opportunity for a quick win through automation. I coaxed her back to her homework. “Come on, let’s set up a spreadsheet,” I said. “That way, you don’t have to redo each of the calculations or worry about getting things wrong.”

We brought up OpenOffice.org Calc. She was still lackluster, so I took the lead in creating the spreadsheet. I asked her which tax return we could use as a model, and she picked hers. We started filling in the formulas, checking her work along the way. (We found and fixed an error in her tax return, too!) Then we tested the spreadsheet on a few other tax returns she had manually done, and she used it to check the rest.

Result: Not only could she verify a correct tax return in less than a minute, but she perked up and started having fun with it. She made a pile of correct tax returns and a pile of incorrect ones, with sticky notes pointing out the deficiencies. She still doesn’t want to be an accountant again, but at least she knows that tedious tasks might be automated away.

The next time J- finds herself doing tedious calculations or verifications, I hope she thinks about how much faster, more reliable, and more enjoyable the spreadsheet was compared to calculating things step by step, and perhaps invest time into learning how to automate whatever she needs to do.

How do people learn how to automate? It’s such a time-saving skill, but it doesn’t seem all that common. Maybe people are intimidated by spreadsheets and programming languages, and that fear of losing more time keeps them from gradually building the knowledge they need to save lots of time. If we can show J- and other kids the benefits of automating, maybe that light at the end of the tunnel will encourage them to learn. If we expose them to the methods for automating tasks, such as putting calculations into a spreadsheet, creating keyboard macros, or writing short programs, maybe they’ll realize it’s not scary – and maybe they’ll start modifying or creating new tools.

In my experience, working with new automating frameworks is always slow and somewhat frustrating in the beginning. It helps that I don’t usually need or want to automate everything right away. I break things down into small things, small wins. I might start by figuring out the most time-consuming parts and automating that 10%, or automating the most common operations. As I become more familiar with the tools and the process, I automate a little bit more, and more, and more. Eventually I might even create a tool that other people can use, like the way my Community Toolkit for Lotus Connections is off and running.

The hardest thing, I guess, is knowing where to start. I run into that problem a lot, because I work with lots of different technologies and frameworks. It’s like looking for the end of a tangled piece of string. That can be hard to find in the confusion, but once you do, you can start unknotting the mess. I want J- to be able to think: ah, this has to do with calculations, maybe I can get a handle on it by using a spreadsheet, putting in manual steps if needed.

How do you use teachable moments to encourage people to automate?

2011-03-29 Tue 21:09

Saving team members from RSI

Posted: - Modified: | geek, work

I watched Jen: Ctrl-C, Alt-Tab, click, click, Ctrl-v, click, click, click, click, click, Alt-Tab, Down, Ctrl-C… One by one, Jen copied the tasks from our Drupal planning spreadsheet to the Rational Team Concert task-tracking system.

I didn't know if RTC had a batch import system we could use, but I'd rather do a macro using AutoHotkey instead of letting Jen copy the information one row at a time. (And with so many clicks, too!)

Fifteen minutes and some tweaking later, I have an AutoHotkey script that copies the information, creates the task, and moves to the next row. A few minutes, and I've copied all the rest of the tasks.

Less risk of repetitive strain injury for everyone, more interesting work, and the ability to easily handle future spreadsheets. Yay!

I show her the AutoHotkey script at work. “Coool,” she says.

Time to organize the tasks by story. Drag-and-drop to the rescue. Not easy with a mouse – Fitts's law, small targets – but it's easy enough with the tablet stylus. It feels natural.

Keep an eye out for the little things that you can fix with just the right tool. =)

AutoHotkey script:

F12::MakeRTCTask()
MakeRTCTask()
{
   SetTitleMatchMode,2
   CoordMode Mouse, Screen
   WinActivate, Planning
   WinWaitActive, Planning
   Send ^c
   Sleep 200
   WinActivate, IBM Rational Team Concert
   WinWaitActive, IBM Rational Team Concert
   Click 972, 346  ; add
   Sleep 500
   Click 927, 406  ; task
   Sleep 500
   Click 468, 154  ; summary text field
   Send ^v
   Send {TAB}{TAB}
   Sleep 100
   Send {DOWN}  ; filed against
   Send {TAB}{TAB}{TAB}{TAB}
   Sleep 100
   Send 1  ; priority
   Send 1
   Click 807, 125  ; save and close
   Sleep 500
   Send {PgUp}{PgUp}{PgUp}
   WinActivate, Planning
   WinWaitActive, Planning
   Send {ESC}{DOWN}
}

Old notes on staffing a virtual conference booth

| conference, connecting, event

It’s fantastic how a blog archive lets me pull up lessons learned from a virtual conference I helped at two years ago. Some of these tips from my internal blog post are platform-specific, but others might be useful.

Staffing the Social Networking booth at the Innovation in Action event. Here are quick tips:

  • Set up text shortcuts. You'll need to type in a lot of text rapidly. The built-in Text Entries are not available when you're sending an initial message or inviting someone to a chat, so type in some boilerplate text into Notepad and then copy and paste it. Messages you send from the booth will be marked as from your booth name, so include your name and e-mail address in your message. Advanced tip: use AutoHotkey to create a text macro. Install it from AutoHotkeyInstaller.exe, create a file like shortcuts.ahk (customize this of course), then double-click shortcuts.ahk to make it part of your system. Example shortcuts.ahk:

    ::!hello::Welcome to the IBM social networking booth. I'm Sacha Chua ([email removed]), a consultant who helps organizations figure out what Web 2.0 is, how it fits with their strategy, how to implement it, and how to make the most of it. Please feel free to ask me questions by sending a note or inviting me to chat. What can I help you with?
    ::!tapscott::Hello and welcome to the IBM social networking booth. I'm Sacha Chua ([email removed]), an IBM consultant who helps organizations figure out what Web 2.0 is, how it fits with their strategy, how to implement it, and how to make the most of it. What did you think of Don Tapscott's keynote? Please feel free to start a chat if you want to talk about it or if you have any questions about social networking.

    After that, you'll be able to type !hello into anywhere and have it expanded. To update, edit shortcuts.ahk and then double-click it again.

  • Check people's visitor histories. The visitor history will tell you about any messages sent from or to this booth, if the visitor has been to this booth before, and so on. Great way to make sure you don't send a message twice.
  • Send people messages and invite them to chat with you. You can initiate only one chat at a time, and you have to wait for the person to accept or reject the invitation before inviting another person. You can send as many messages as you want, though, and you can have as many open chats as you want.
  • Send yourself follow-up requests after conversations. Your goal in each conversation is to find out what people are interested and give yourself an excuse to follow up. After you get that, use the [i] button on the right (your chat partner's profile) to display the profile, then use the Followup button to send yourself a copy of the person's visitor history. WARNING: There's some delay when selecting names from the list, so double-check that you're sending the right person's information.
  • Pull in experts. Need help answering a question? Tell the visitor you're bringing someone in, then click on the expert's profile, choose Invite to chat, and choose the chat session you want the expert to join.

Non-obvious things:

  • Your name will not be associated with any messages (from or to), so don't count on being able to quickly see replies from people or find out what you sent someone.
  • The sorting buttons on the lists sort only the displayed entries, not all the entries. Entries will always be arranged chronologically, although in-page sorting may be different. Don't count on being able to use this to see all the messages sent by visitors. Just leave it on Date.
  • If someone leaves your booth while you're trying to check their visitor history, their info box disappears.
  • As people enter and leave the booth, odd things happen to the page. Be prepared to have to find people again.
  • Things get much quieter when people are listening to sessions. Eat or rest during those times.

Braindump: Automating repetitive tasks using AutoHotkey

Posted: - Modified: | braindump

Note for myself (because I'm going to need this again someday!), and for others who drop by:

I needed to copy information from 45 slides and put them into an Excel spreadsheet so that I could reorganize the content and put them into a wiki. Fortunately, the author of the Powerpoint deck used a fairly consistent slide format. I used AutoHotkey to copy most of the information over by simulating mouse clicks and button presses. I started with this macro, which copies the text, switches to my spreadsheet, moves a cell to the right, and pastes it:

F12::
Send, {CTRLDOWN}c{CTRLUP}
WinWait, Microsoft Excel - facilitation.xls, 
IfWinNotActive, Microsoft Excel - facilitation.xls, , WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{CTRLDOWN}v{CTRLUP}{ALTDOWN}{ALTUP}
WinActivate, Microsoft PowerPoint
return

 

I wanted to save even more keystrokes and mouseclicks, so I ended up automating the copying of each slide using the following script. It wasn’t perfect, but it saved me time and it was fun to make.

F11::
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left, 1037,  327, 1500, 327
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {CTRLDOWN}v{CTRLUP}{RIGHT}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left,  1037,  366, 1500, 366
Sleep, 100
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{CTRLDOWN}v{CTRLUP}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left,  457,  344, 1500, 1000
Sleep, 100
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{RIGHT}{CTRLDOWN}v{CTRLUP}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left,  454,  454, 1500, 1000
Sleep, 100
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{CTRLDOWN}v{CTRLUP}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left,  564,  535, 1500, 1000
Sleep, 100
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{CTRLDOWN}v{CTRLUP}{ALTDOWN}{ALTUP}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
MouseClickDrag, left,  490,  637, 1500, 1000
Sleep, 100
Send, {CTRLDOWN}c{CTRLUP}
WinActivate, Microsoft Excel - facilitation.xls, 
WinWaitActive, Microsoft Excel - facilitation.xls, 
Send, {RIGHT}{CTRLDOWN}v{CTRLUP}{LEFT}{LEFT}{LEFT}{LEFT}{LEFT}{LEFT}{DOWN}
WinActivate, Microsoft PowerPoint - [ID Methods.ppt], 
WinWaitActive, Microsoft PowerPoint - [ID Methods.ppt], 
Send, {PGDN}
return

Automation is worth the time investment. If you're on Windows, check out AutoHotkey. =)