I thought it would be a good idea to use RSpec for creating custom Rake TaskLib's, but I couldn't find any examples for this. So here's a pretty simple one from a project I'm working on at the moment. A TaskLib for creating a Mono resource generator task.
This TaskLib should create a file task for a *.resources file, depending on a *.resx file:
require 'rake'
describe ResgenTask do
it 'should create a file task with the *.resource file as target and *.resx as dependency' do
ResgenTask.new 'foo.resources'
Rake::Task.tasks.should satisfy do |tasks|
for task in tasks
if task.name == 'foo.resources' and task.prerequisites.include?('foo.resx')
break true
end
end
false
end
end
end
This doesn't work because of the missing ResgenTask, so we create it:
require 'rake/tasklib'
class ResgenTask < Rake::TaskLib
def initialize(target)
end
end
The specification now fails:
>spec resgentask_spec.rb --format specdoc
ResgenTask
- should create a file task with the *.resource file as target and *.resx as dependency (FAILED - 1)
1)
'ResgenTask should create a file task with the *.resource file as target and *.resx as dependency' FAILED
expected to satisfy block
./resgentask_spec.rb:11:
Finished in 0.005009 seconds
1 example, 1 failure
But before we implement this specification, let's refactor it a little bit. This is much more readable:
it 'should create a file task with the *.resource file as target and *.resx as dependency' do
ResgenTask.new 'foo.resources'
Rake::Task.tasks.should have_file_task('foo.resources').depending_on('foo.resx')
end
Because there is no have_file_task, we need to create a custom matcher:
module RakeMatchers
class FileTaskMatcher
def initialize(filename)
@target = filename
@source = nil
end
def depending_on(source)
@source = source
return self
end
def matches?(tasks)
@tasks = tasks
for task in tasks
if (task.name == @target) and (not @sources or task.prerequisites.include?(@source))
return true
end
end
return false
end
def failure_message
"expected file target #{@target} with dependency #{@source} to be in task list [#{@tasks.join(' ')}]"
end
def negative_failure_message
"expected file target #{@target} with dependency #{@source} not to be in task list [#{@tasks.join(' ')}]"
end
end
def have_file_task(filename)
return FileTaskMatcher.new(filename)
end
end
In order to be able to use this matcher, we simply include it:
describe ResgenTask do
include RakeMatchers
it 'should create a file task with the *.resource file as target and *.resx as dependency' do
ResgenTask.new 'foo.resources'
Rake::Task.tasks.should have_file_task('foo.resources').depending_on('foo.resx')
end
end
It still fails, but with a much nicer output:
>spec resgentask_spec.rb --format specdoc
ResgenTask
- should create a file task with the *.resource file as target and *.resx as dependency (FAILED - 1)
1)
'ResgenTask should create a file task with the *.resource file as target and *.resx as dependency' FAILED
expected file target foo.resources with dependency foo.resx to be in task list []
./resgentask_spec.rb:52:
Finished in 0.005116 seconds
1 example, 1 failure
Time to implement this specification:
class ResgenTask < Rake::TaskLib
def initialize(target)
file target => target.gsub('.resources', '.resx')
end
end
The next specification will describe the behavior of the file task:
it 'should create a file task that runs resgen2' do
ResgenTask.new 'foo.resources'
end
But wait a moment! Before letting ResgenTask.new create a new file task, the old tasks should be dropped:
describe ResgenTask do
include RakeMatchers
before :each do
Rake::Task.clear
@tasklib = ResgenTask.new 'foo.resources'
end
Ok - now we can think about, how to test the generated task. What it should do, is run sh('resgen2 foo.resources foo.resx'). We don't want to actually run sh(), so we need to mock it. sh() is defined in the module FileUtils. My first guess was, to use FileUtils.should_receive(:sh), but this didn't work. To be honest, I have no idea, what's the right place to mock sh(). But the simplest solution is to create a mocked sh() member method on the task lib:
it 'should create a file task that runs resgen2' do
@tasklib.should_receive(:sh).with('resgen2 foo.resources foo.resx')
Rake::Task['foo.resources'].execute(nil)
end
The specification fails, so let's implement it:
class ResgenTask < Rake::TaskLib
def initialize(target)
file target => target.gsub('.resources', '.resx') do |task|
sh("resgen2 #{task.name} #{task.prerequisites}")
end
end
end
To add an extra goody, the compiled resource file should be removed on the clean target automatically:
it 'should add the *.resources file to the clean target' do
CLEAN.should include('foo.resources')
end
This can be implemented as:
def initialize(target)
CLEAN.include(target)
file target => target.gsub('.resources', '.resx') do |task|
The final set of specs now looks like this:
describe ResgenTask do
include RakeMatchers
before :each do
Rake::Task.clear
CLEAN.celar
@tasklib = ResgenTask.new 'foo.resources'
end
it 'should create a file task with the *.resource file as target and *.resx as dependency' do
Rake::Task.tasks.should have_file_task('foo.resources').depending_on('foo.resx')
end
it 'should create a file task that runs resgen2' do
@tasklib.should_receive(:sh).with('resgen2 foo.resources foo.resx')
Rake::Task['foo.resources'].execute(nil)
end
it 'should add the *.resources file to the clean target' do
CLEAN.should include('foo.resources')
end
end
...and is implemented by:
class ResgenTask < Rake::TaskLib
def initialize(target)
CLEAN.include(target)
file target => target.gsub('.resources', '.resx') do |task|
sh("resgen2 #{task.name} #{task.prerequisites}")
end
end
end
It's a pretty simple example, but it should be enough to get started.




