January 23rd, 2009 - by Mark Van Holstyn

Automating easily forgettable tasks

There are many business requirements that we need to remember or ensure that gets done as developers which are not necessarily related to code or the behavior of the software. This could be code comments, copyright notices, or other information that are not necessarily executable, but part of the source code. For these types of requirements, we used rSpec to explicitly check whether the behavior-less requirement was met throughout our code base.

On one of the projects we have been working, our client has requested that we add a header with a copyright notice to the top of each file. The header was a few lines long, so we created a TextMate snippet to make it easy to generate when creating a new file. Unfortunately, this did not help us remember to actually add the header to each new file. After continually forgetting to add the header, we decided to create a spec which checks that all files have the appropriate header to help us remember.

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
describe "copyright header" do
  it "should be required on all ruby files" do
    vendor = %w[
      db/schema.rb lib/tasks/rspec.rake ...
    ]
    
    (Dir["{app,lib,db,spec,features}/**/*.{rb,rake}"] - vendor).each do |filename|
      filename.should have_ruby_copyright_header
    end
  end
  
  it "should be required on all javascript files" do
    vendor = %w[
      public/javascripts/all.js public/javascripts/prototype.js ...
    ]
    
    (Dir["public/javascripts/**/*.js"] - vendor).each do |filename|
      filename.should have_javascript_copyright_header
    end
  end
  
  it "should be required on all stylesheet files" do
    vendor = %w[
      public/stylesheets/all.css public/stylesheets/blueprint/screen.css ...
    ]
    
    (Dir["public/stylesheets/**/*.css"] - vendor).each do |filename|
      filename.should have_stylesheet_copyright_header
    end
  end
end
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
class CopyrightHeaderMatcher
  def initialize(comment_line, comment_start = nil, comment_end = nil)
    @header = <<-EOHEADER
#{comment_start || comment_line} Copyright (c) 2008 Your Organization
#{comment_line}
#{comment_line} Possession of a copy of this file grants no permission or license
#{comment_line} to use, modify, or create derivate works.
#{comment_line} Please visit http://www.example.com/contact for further information.
EOHEADER
    @header << "#{comment_end}\n" if comment_end
  end
  
  def matches?(filename)
    @filename = filename
    File.read(filename).match(Regexp.new(Regexp.escape(@header))) ? true : false
  end
  
  def failure_message
    "expected #{@filename} to have the header:\n#{@header}"
  end
end
 
def have_ruby_copyright_header
  CopyrightHeaderMatcher.new("#")
end
 
def have_javascript_copyright_header
  CopyrightHeaderMatcher.new("//")
end
 
def have_stylesheet_copyright_header
  CopyrightHeaderMatcher.new(" *", "/*", " */")
end

Now when we forget to add the header, we get a nice reminder when running the specs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ script/spec spec/code/header_spec.rb
F..
1)
'copyright header should be required on all ruby files' FAILED
expected app/models/user.rb to have the header:
# Copyright (c) 2008 Your Organization
#
# Possession of a copy of this file grants no permission or license
# to use, modify, or create derivate works.
# Please visit http://www.example.com/contact for further information.
 
Finished in 0.314035 seconds
 
3 examples, 1 failure

Problem solved.

1 Comment

Apostlion

Sick stuff!

Actually, I think you can very well do tests to maintain the general coding style (if it is set organization-wise) — check stuff like indentation, braces positioning and comment placement.