Flip side

Posted by Luke
on Saturday, January 26

Obie is getting some links by showing off this graph which demonstrates the percentage growth of Ruby versus Java jobs:

Having come from the Java world and loving Ruby/Rails development, I’m happy with this. However, there is a flip side. Take a look at the absolute number of jobs posted:

We still have a long way to go.

Measuring your test coverage with Heckle and RCov

Posted by Jon
on Thursday, November 29

I gave a presentation at RUM on Monday about code metrics. In particular, I showed tools for measuring two aspects of code: test coverage and complexity. Here are my slides.

Saikuro and Flog measure code complexity. Saikuro measures cyclomatic complexity, the number of independent paths through a method. Flog, on the other hand, parses your code and assigns a complexity value to assignments, branches, and calls. The goal, of course, is to minimize code complexity. This is an important goal, but I’m not sure yet what I think of these measurement tools. I haven’t used them enough to know if they have practical value.

Heckle and RCov on the other hand, are useful. I’m going to look at each in more detail here.

RCov

RCov measures C0 code coverage. That is, it runs your test suite, and looks at what lines of your application were run or not run. It then gives you a nice HTML report with red and green lines – red for lines of code that are not run, and green for lines that are run.

If your test suite doesn’t execute a line of your application code, it is safe to say that that line is not tested. On the other hand, if a line of your application is run, it is NOT safe to say that it IS tested. A test method with no asserts works just fine for RCov’s purposes, thank you very much. Take a look at this code.

def test_user_assignment
  User.assign
end

This test is enough to mark the User.assign method as tested. But nothing is asserted, and so nothing is tested. The problem is equally true even if you aren’t in the habit of writing tests without assertions; you may make assertions about some aspects of a method, but forget about other aspects. And RCov won’t tell you this.

Logically speaking, RCov tells you that if line_is_red, then !line_is_tested. From this, you can also infer the contrapositive: if line_is_tested, then !line_is_red. But that’s all you know. If a line is green, RCov tells you nothing at all. Saying if !line_is_red, then line_is_tested is a formal fallacy (denying the antecedent). And that’s bad.

So 100% RCov coverage is not equal to 100% test coverage. In fact, the two have nothing to do with each other. Your code could have 100% or 95% or 75% RCov coverage, and be extremely poorly tested.

In my experience, RCov is a one-time tool. That’s because green lines in RCov don’t tell you anything at all about your test coverage. Red lines provide the real value. If you run RCov, find an untested method, and write up a quick test hack that provides C0 coverage, RCov will never complain about that method again. It will be off your RCov radar. This is too bad, because it is really useful to know what is poorly tested. So whenever you see red in RCov, take the time to write comprehensive tests to cover the untested code.

Heckle

Heckle is a mutation tester that changes your code and checks to see whether your tests catch the changes. If Heckle is able to change instances of true to false (or 32 to nil, or remove method calls) in your application without creating a test failure, then your code isn’t tested well enough. To run it effectively, do this:

heckle Class method -t /test/units/class_test.rb -T 30

heckle is the tool, installed as a Ruby gem. Class is the name of the Ruby class you want to heckle. method is a method on the class; you can leave this out, but I don’t recommend it. -t /test/units/class_test.rb is the path to the unit test you want to use (also optional). Finally, -T 30 specifies a timeout for the test, in case your mutation creates an infinite loop.

You can leave out the last three options and just run Heckle with a class:

heckle Class

But I don’t recommend it.

First, it will take forever.

Second, you may run into infinite loops.

Third, heckle will unfortunately test EVERY method available to a class, including methods included by modules, superclasses, etc. So if you’re heckling an ActiveRecord class, you’re going to see dozens of Rails magic methods, not just the methods that you wrote.

Fourth, your UserTest should cover your User class on its own, if your code is well written and well tested; it shouldn’t rely on the ProductTest class (or another test). One problem with Heckle is that it doesn’t distinguish between well tested code and highly coupled code, where a small change somewhere causes the application to fall apart somewhere else. This problem can be minimized by only comparing a single method to a single test class.

I like Heckle and find it pretty useful. Unfortunately, it needs a little developer love. The -T timeout parameter is flaky; it doesn’t always play nice with its dependencies (especially ParseTree 2.0.x, the current version); and it would be more useful if by default it only heckled the methods directly added by a class, not methods brought in through parent classes, includes, or fancy metaprogramming. This is a shame, because it is really a great tool. Hopefully Kevin Clark and Ryan Davis have an update in the works.

Slantwise at Rubyconf

Posted by Jon
on Thursday, November 01

Three members of the Slantwise team will be heading out to Charlotte tonight for Rubyconf 2007 – Dan Weinand, Luke Francl, and Jon Dahl. We’re excited and hoping that it will have some of the magic of earlier Ruby conferences. Railsconf 2007 was a good time, but things are bound to change when O’Reilly and a thousand attendees are involved.

A few other folks from the Twin Cities are going too: Tom Brice, and three members of the JRuby team: Nick Sieger, Thomas Enebo, and Charles Nutter. (As an aside, JRuby is probably the most exciting thing going on in the Ruby world these days – even for non-Java programmers like yours truly – so if you aren’t following the JRuby project, you really should. At RUM on Monday, Nick and Charlie demonstrated deploying Rails apps with Glassfish, and the JRuby compiler, respectively. Pretty cool.)

Here is a picture of Jon, Luke, and Dan (left to right). So if you see us at the conference this weekend, say hi!

RVideo 0.9 is now available

Posted by Jon
on Tuesday, October 02

RVideo is now available as a Ruby gem. Install with:

sudo gem install rvideo

(RVideo depends on other tools for transcoding, like ffmpeg, so you’ll probably need to install a few other things as well. See the Documentation for a little more detail.)

I’ve tagged this release as 0.9.0. It is still beta-quality code, so test thoroughly. If you run into problems, let me know – I’ll be deploying RVideo to a live app soon, so I want to squash any bugs as much as you do. :)

What is it?

RVideo is a Ruby library for video/audio transcoding. It provides a clean Ruby interface to transcoding tools like ffmpeg, and can easily be extended to support more tools. At this point, only ffmpeg and flvtool2 are supported, but more will follow.

1
2
transcoder.execute(recipe, {:input_file => "/path/to/input.mp4",
      :output_file => "/path/to/output.flv", :resolution => "640x360"})

Details

To inspect a file, initialize an RVideo file inspector object. See the documentation for details.

A few examples:

1
2
3
4
5
6
7
8
9
  file = RVideo::Inspector.new(:file => "#{APP_ROOT}/files/input.mp4")

  file = RVideo::Inspector.new(:raw_response => ffmpeg_inspection_response)

  file = RVideo::Inspector.new(:file => "#{APP_ROOT}/files/input.mp4",
                                :ffmpeg_binary => "#{APP_ROOT}/bin/ffmpeg")

  file.fps        # => "29.97"
  file.duration   # => "00:05:23.4"

To transcode a video, initialize a Transcoder object.


  transcoder = RVideo::Transcoder.new

Then pass a command and valid options to the execute method

1
2
3
4
5
6
7
8
9
  recipe = "ffmpeg -i $input_file$ -ar 22050 -ab 64 -f flv -r 29.97 -s"
  recipe += " $resolution$ -y $output_file$"
  recipe += "\nflvtool2 -U $output_file$"
  begin
    transcoder.execute(recipe, {:input_file => "/path/to/input.mp4",
      :output_file => "/path/to/output.flv", :resolution => "640x360"})
  rescue TranscoderError => e
    puts "Unable to transcode file: #{e.class} - #{e.message}"
  end

If the job succeeds, you can access the metadata of the input and output files with:

1
2
  transcoder.original     # RVideo::Inspector object
  transcoder.processed    # RVideo::Inspector object

If the transcoding succeeds, the file may still have problems. RVideo will populate an errors array if the duration of the processed video differs from the duration of the original video, or if the processed file is unreadable.

RVideo supports any transcoding tool with a command-line interface; adding a new tool just means writing a class for the tool that subclasses RVideo::AbstractTool. It also means that you need to use common sense to avoid attacks. For example: don’t run RVideo as a privileged user. Control your input recipes, and don’t accept user-submitted recipes. (RVideo is pretty well protected from these problems; you can’t execute a command that isn’t identified by a transcoder tool class, so `rm -rf *` won’t work. But it pays to be cautious.)

More info

See the RVideo Google Code site for more info, including links to Documentation and a Google discussion group. Use these to file tickets, discuss, etc. (The SVN repository is currently at Rubyforge, but I may move it to Google Code.)

Contribute

I would love help on this project. If you want to help out, there are a few things you can do.

  • Use, test, and submit bugs/patches
  • We need a RVideo::Tools::Mencoder class to add mencoder support. (Someone has started on this, so let me know if you’re interested in helping and I’ll put you in touch.)
  • Other tool classes would be great – On2, mp4box, Quicktime (?), etc.
  • Eventually, it would be great to (optionally) use the processing feedback provided by ffmpeg etc. to get real-time progress updates (e.g. 20% complete, 40% complete, 90% complete). (More info)
  • Submit other fixes, features, optimizations, and refactorings

RVideo is alive - really

Posted by Jon
on Thursday, September 13

It’s been a while since I posted about RVideo. Consider this a pre-release announcement.

RVideo is a Ruby gem that makes video and audio transcoding a bit easier. The gem wraps various video transcoding tools – ffmpeg and flvtool2 upon release. But it is extensible, so that other tools (like mencoder) can be added as needed. Transcoder instructions are specified by passing a recipe to RVideo, along with custom values.

For example:

1
2
3
4
5
6
recipe = "ffmpeg -i $input_file$ -r 29.97 -vcodec xvid -s $resolution$ $output_file"

transcoder = RVideo::Transcoder.new
transcoder.execute(recipe, {:input_file => "original.mov", 
                            :output_file => "processed.mp4", 
                            :resolution => "640x360"})

The resulting transcoder object will include information about the job, including metadata for the output file, etc. The execute method raises a variety of exceptions when it doesn’t work (e.g. input file not found, unsupported formats, could not save the output file), so you’ll want to run #execute in a begin/end block and handle each exception as needed.

I’ll go into more detail in a later post, so look for more updates soon. Expect the gem to launch sometime in October.

Subversion hooks in Ruby

Posted by Luke
on Sunday, August 19

Your source control system is where the knowledge of your team is consolidated and requirements are turned into working code. That process is recorded in the change history and commit comments of the SCM. Hook scripts help you integrate that knowledge into the rest of your development process. I’ll write about Subversion because it’s what I use, but every SCM worth its salt has similar facilities.

Let’s say I’d like to integrate my commit messages with my bug tracker. Systems like CVSTrac and Trac have made this popular, and it’s really useful. At my last job, I wrote a Python script that submitted our commit messages to Bugzilla, which was what we used.

Just for fun, I decided to re-implement it in Ruby using ActiveRecord and the latest version of Bugzilla. Ruby is a nice language for writing Subversion hooks because it has a lot of useful libraries, and it’s easy to run other executables from ruby with ``. Plus, you can still read the code six months later!

Here’s how it works:

When a commit is submitted to Subversion, the post-commit hook runs svn2bugzilla.rb. This script uses svnlook to extract the commit information and searches for strings like “bug #123”, then creates a new comment in Bugzilla including the commit message, the revision, and the files changed for each bug found.

There’s two things I needed to do to get this working:

  1. First, I had to create ActiveRecord classes for the Bugzilla tables representing a bug (bugs, a comment (longdescs), and a user (profiles). These classes don’t use the ActiveRecord conventions, so I had to work around that. The longdescs class has a type column, which ActiveRecord does not like (this strikes me as a major problem for using AR with legacy databases).
  2. Second, I had to use svnlook to get the information I need.

The power of svnlook

Subversion post-commit hooks work by executing a script called hooks/post-commit (note: this script must be executable. Change the file permissions if it’s not working!). By convention, hooks/post-commit should call off to other programs to perform the work. To that end, it provides you with two variables: the repository location, and the commit number.

Using svnlook you can then extract some very useful information from the repository given the revision number. Here’s just a few of the things svnlook can tell you: author, cat (show the files changed), changed (list the files changed), date, diff, and log.

For my script, I needed to know: author, changed, and log. Using them, I can create a message like this:

Bug #1: Re-org for configuration; add comments for clarity.

Revision: 8

Changes:

U   svn2bugzilla.rb

svn2bugzilla.rb

Here’s the code. All the configurable options are at the top of the script. If your subversion user names are not the same as your bugzilla usernames, you can map them in USER_MAP. Then, configure your svnlook location and database connection information and you’re done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/local/bin/ruby
require 'rubygems'
require 'active_record'
require 'set'

# If your Subversion usernames are not the same as your 
# Bugzilla usernames, map them here.
USER_MAP = {"luke" => "luke@slantwisedesign.com"}

# Location of svnlook binary. Change as necessary.
SVNLOOK = "/usr/local/bin/svnlook"

# Configure your AR connection here. 
# Bugzilla supports both MySQL and PostgreSQL.
AR_CONFIG = {:adapter => 'mysql', 
             :database => 'YOUR_BUGZILLA_DB', 
             :username => 'YOUR_BUGZILLA_DB_USER', 
             :password => 'YOUR_BUGZILLA_DB_PASS' }

# You should not have to change anything below this line.

if ARGV[0].nil? || ARGV[1].nil?
  puts "Usage: svn2bugzilla.rb repos_path revision"
  puts "To be used as a subversion post-commit hook."
  exit
end

REPOS_PATH = ARGV[0]
REVISION = ARGV[1]

ActiveRecord::Base.establish_connection(AR_CONFIG)

# These are the three Bugzilla tables we'll be dealing with.
# It'd probably be less code just to query the database directly, 
# bug using ActiveRecord is more fun!

class Bug < ActiveRecord::Base
  set_primary_key "bug_id"
  # longdescs has a column named 'type' which doesn't play well with AR.
  # select the columns we need manually.
  has_many :longdescs, :select => "comment_id, bug_id, who, bug_when, thetext"
end

# longdescs is the comments table.
class Longdesc < ActiveRecord::Base
  set_primary_key "comment_id"
  belongs_to :bug
  belongs_to :profile, :foreign_key => "who"
end

# profiles is the user table
class Profile < ActiveRecord::Base
  set_primary_key "userid"
end

class Commit
  def initialize(repository_path, revision_number)
    @revision_number = revision_number
    @log_message = `#{SVNLOOK} log #{repository_path} -r #{revision_number}`.strip
    @files_changed = `#{SVNLOOK} changed #{repository_path} -r #{revision_number}`
    @author = `#{SVNLOOK} author #{repository_path} -r #{revision_number}`.strip
  end
  
  def message
    <<MESSAGE
#{@log_message}


Revision: #{@revision_number}

Changes:

#{@files_changed}
MESSAGE
  end

  def author
    if USER_MAP[@author].nil? 
      return @author
    end
    
    USER_MAP[@author]
  end
  
  # return a Set of unique bug numbers in the commit message
  def bug_numbers
    bugs = Set.new
    @log_message.scan(/bug\D{1,3}(\d+)/i).each do |match|
      bugs << match[0]
    end
    
    bugs
  end
end

# Do the actual work of submitting the comment to the database

commit = Commit.new(REPOS_PATH, REVISION)
commit.bug_numbers.each do |bug|
  bug = Bug.find_by_bug_id(bug)
  
  next if bug.nil?
  
  user = Profile.find_by_login_name(commit.author)
  
  next if user.nil?
  
  bug.longdescs.create(:who => user.id, 
                       :thetext => commit.message, 
                       :bug_when => Time.now)
end

Configuring hooks in post-commit

By default, there is no hooks/post-commit file for a Subversion repository. You need to copy the template file named post-commit.tmpl to post-commit and chmod it so it’s executable.

Then, remove any examples from the post-commit script, and add svn2bugzilla.rb:

1
2
3
4
REPOS="$1"
REV="$2"

/usr/local/bin/ruby /path/to/script/svn2bugzilla.rb $1 $2

You can read more about Subversion hooks in the manual.

Testing the script

Testing glue code like this is a bit of a pain because it doesn’t exist on its own. If you install it as a hook and it’s not working, you won’t get any feedback. Since it’s a post-commit hook, the commits will succeed just fine even if the script’s not working.

To test it, you can run the script by hand with svn2bugzilla.rb /path/to/repos rev_number and see what happens.

How not to apply for a job

Posted by Jon
on Wednesday, April 25

We may be looking for some help this summer (and beyond), so we put a Ruby Developer Wanted post on Craigslist. We’ve had great success with Craigslist in the past; we found two of our employees and two of our key contractors there. But of course, the signal to noise ratio is lower than we would like. We usually get form-letter offers from offshore developers offering .NET or Java skills (even though we specifically ask for local Ruby developers).

This last time, we got a priceless email from an applicant that I’ll call Ivan (not his real name). Ivan’s email started OK – “saw your Craigslist ad and I’m interested,” etc. Quickly, though, problems came to the surface.

  • Ivan doesn’t do Ruby work. He’s mostly offering design, along with PHP and ASP.
  • Second, Ivan won’t work onsite, even though he appears to have an area code in the Twin Cities.
  • Third, Ivan doesn’t really appear to be looking to do any work himself. He is offering to subcontract the work to others. “I can staff as little as 1 part time, to as much as you need (50+ full time designers).”

This isn’t remarkable so far. I usually get a few of these when posting to Craigslist. But take a look at the next paragraph:

“Let me explain how we work a little bit here. I have system surveillance software installed on the computer which will send you an E-mail every X minutes with a screenshot of my computer (50k in size each). This way you can be sure that I am working on your project at the scheduled shift and you can see the quality of work as it is being produced. You can also use this to confirm that I came in on time to my shift, and left on time, etc…”

This is wrong for so many reasons.

1. I don’t wan’t to sift through X (10? 500?) screenshots every day to make sure that my contractors are doing their job. Nor do I have time. The reason we might need a contractor is that we’re too busy to do the work ourselves.

2. Seeing screenshots taken from someone’s computer just feels slimy, like an intrusion of privacy. Even if Ivan is sending them to me (instead of me stealing them from him), it is not something I want to do.

3. How absurdly easy would it be to fake something like this? Heck, it would probably be easier to fake it than to do it for real.

4. We don’t hire people based on the idea that they will sit at their desk for 8h/day. We hire people to get things done. This is a misdirected approach to productivity.

And of course, the only point that really matters:

5. Ivan has destroyed any sense of trust. He’s asking me to expect deceit, and giving me a means to protect myself against his deceit (and an unreliable one at that). There is no way in hell that I would work with a contractor who I didn’t trust, no matter how many screenshots he offers me.

As I think about it, trust is even more important than competence. I’d rather have a trustworthy employee who made mistakes than a genius who I didn’t trust. Fortunately for Slantwise, we’ve been able to find both. I don’t think I’ll break our streak by hiring Ivan.

Video Transcoding: part 0 in a N part series

Posted by Jon
on Sunday, April 22

(Update: I gave a talk on video transcoding at MinneBar, and the slides are now available online.)

Video on the web is a hot topic these days. Hundreds of people want to be the next YouTube, and thousands more are making use of user-submitted in some other way.

Unfortunately, putting video on the web is a total mystery to some developers, and may seem deceptively simple to others. The former don’t know where to start, and couldn’t tell a codec from a container, or ffmpeg from mencoder. The latter know the fundamentals, which don’t look too tough, but would have trouble putting together a scalable, robust, production-worthy system.

I’m not an expert, but I have been a part of several projects which do video transcoding. So in my next several posts, I will outline video transcoding for the web from a variety of angles.

  • Part 2: Tools – what are the free (e.g. ffmpeg) and commercial (e.g. On2 Flix) tools that can be used to build a transcoding system?
  • Web application integration – once I’ve settled on codecs and tools, how do I put everything together into a working system?
  • Legal and licensing issues – some prominent codecs and formats are commercial; some are completely free; and some of the most popular occupy an ugly middle ground that require royalties.
  • RVideoSlantwise is working on a Ruby-based video transcoding library called RVideo. I’ll outline its capabilities and the various design decisions we made (and are making) along the way.

A few parts of this series will have a Ruby on Rails focus, but most of the information is generic. So if Ruby isn’t your thing, you may still want to stick around.