How A .NET Developer Hacked Out A Rake Task
I spent the last two days having my brain melted by Scott Bellware, in a crash course on Ruby, Rake, RSpec, Selenium, web UI test automation best practices, and all-around uber-normal-person-language-oriented development. It was great! After the first 7 hour day, I felt like I had been awake for 24 hours. After the second 8 hour day, I felt like I actually knew something about Ruby and good web UI test automation practices.
Will The Real Rake Task Please Stand Up
In the conversations and snappy banter that ensued during and after the sessions, Scott and I talked a little about Rake and building what he referred to as ‘real’ Rake tasks instead of the ‘not-Rakeish’ tasks that most people build. By ‘not-Rakeish’, Scott was talking about our propensity as .NET developers to make everything a class and make heavy use of the “system.Object of Rake: the ‘task’”. This point is illustrated by a google search of ‘build custom rake task’. Practically every result that comes up will show you how to use “task :somename do … end” and call “rake :somename”.
Whether or not you think using standard ruby objects in a standard rake ‘task’ is ‘Rakeish’ or not, isn’t really the point. Of course that works. Yes, many people do it that way, because it works. The point Scott was making is that professional tools like RSpec and Selenium don’t provide tasks in that way. We don’t see these simple constructs being used. Rather, we see professional tasks being built so that we can call something more like this:
1: Spec::Rake::SpecTask.new do |t|
2: t.ruby_opts = ['-rtest/unit']
3: t.spec_files = FileList['test/**/*_test.rb']
4: end
</div> </div>
That conversation with Scott and some other conversations around the idea of building .NET solutions with Rake got me thinking about how we could build a much more ‘professional’, ‘real’ Rake task to call out to MSBuild. While I have not yet put this solution together, I have spent some time learning how to build tasks such as the RSpec ‘SpecTask’ shown above. Not surprisingly, it’s a very simple endeavor. I honestly don’t claim to understand exactly why everything in my hacked-together custom task is set up the way it is, but I have a pretty good idea of what’s going on, at this point. So, I wanted to share my newfound knowledge and hopefully inspire some of the .NET Rake building people out there to create something a little more ‘Rakeish’, rather than just using the default ‘task :whatever’ syntax and calling into an ‘MSBuild’ object.
Defining A Custom Rake Task Using TaskLib
Since I was unable to find any good info via my google search, I started examining the RSpec rake task and the Selenium rake tasks that were installed via the gems. The first thing I found is that I needed to have the ‘rake’ and ‘rake/tasklib’ files, so that I could create a class that inherits from ‘TaskLib’. For example, I can create a task called “FooTask” like this:
1: require 'rake'
2: require 'rake/tasklib'
3:
4: class FooTask < Rake::TaskLib
5: end
</div> </div>
This basic definition gives me a task that I can use via Rake. However, it doesn’t really do anything yet and doesn’t even have a name that we can call from rake.
Giving The Task A Name
To set up a good task name to use, we need to include a ‘:name’ accessor (think auto property in c#) and parameter in the initializer. This name will be used later, to tell the TaskLib base class what the name of the task is, at runtime.
1: class FooTask < Rake::TaskLib
2: attr_accessor :name
3: def initialize(name = :some_default_name)
4: @name = name
5: end
6: end
</div> </div>
Notice the ‘:some_default_name"’ – this does exactly what it sounds like… sets the default name of the task. In this case, we are literally giving the name a default of ‘some_default_name’. We are then setting the @name (which has been automatically generated by the ‘attr_accessor :name’ usage… again, this is like an auto property in C#).
Letting The Task Do Custom Work
If we want to provide the ability for the user of the task to actually do some work, we need to include the lambda block evaluation that ruby has built in. This is done by yielding self if a lambda block is present, in the constructor.
1: def initialize(name = :test)
2: @name = name
3: yield self if block_given?
4: end
</div> </div>
this block of code let’s us call our custom task with a ‘do … end lambda’ block
1: FooTask.new do |t|
2: #do stuff with t, here
3: end
</div> </div>
The ‘|t|’ in this code refers to the actual FooTask instance that we yielded (‘self’), and will let us have access to all of the methods in the FooTask object, once we add some.
Defining The Default Work To Do
In all of the custom rake tasks that I have examined so far, I have found a method called ‘define’ that appears to be a standard convention for rake tasks as the location of code that gets executed by the custom task that we are creating. This method calls the TaskLib method ‘task’ with a name parameter (the name we supplied in the initializer) and then does the work. For example, RSpec does all of it’s spec parsing and running from here, and Selenium does all of it’s server initializing or shutdown here. For this example, we’re just going to print some junk out to the screen.
1: def define
2: task name do
3: puts 'some test being printed'
4: end
5: end
</div> </div>
The ‘task name’ code is important here, as it provide rake with the knowledge of the task’s name, so that we can call it from the rake command line.
Running What We Have
At this point, we have enough code in place to run the sample rake task. Create a ‘rakefile’ with all of the above code, then add code similar to how we called the ‘SpecTask’ earlier. The contents of the rakefile should be:
1: require 'rake'
2: require 'rake/tasklib'
3:
4: class FooTask < Rake::TaskLib
5:
6: attr_accessor :name
7:
8: def initialize(name = :test)
9: @name = name
10: yield self if block_given?
11: define
12: end
13:
14: def define
15: task name do
16: puts 'some test being printed'
17: end
18: end
19: end
20:
21: FooTask.new :my_task do
22: end
</div> </div>
Notice that we are giving the task a new name when we call ‘FooTask.new :my_task’. The name is being set to ‘my_task’. We can now run the task from rake via a shell prompt (command prompt in windows):
So we now have a custom rake task that is much more ‘Rakeish’. However, it’s still not terribly useful. There is no way to get data into the task and there is no way to call any dependencies before this task.
Adding Data To The Task
To add data, methods, or do something useful with the task, we can use all the usual ruby ways – methods with parameters, with ‘=’ assignment, attr_accessors, etc etc. For simplicity, let’s use an attr_accessor to create a ‘property’ (I know, it’s not really a property in Ruby, but it sure behaves like one). We’ll add an accessor call ‘:data’ and we’ll have the define method write that data out instead of our hard coded string.
1: class FooTask < Rake::TaskLib
2:
3: attr_accessor :name, :data
4:
5: def initialize(name = :test)
6: @name = name
7: yield self if block_given?
8: define
9: end
10:
11: def define
12: task name do
13: puts @data
14: end
15: end
16: end
</div> </div>
Adding the data accessor will let us use the ‘|t|’ yielded to our task at runtime, like this:
1: FooTask.new :my_task do |t|
2: t.data = "I'm using the data accessor!"
3: end
</div> </div>
Now when we run the task again, we get this output:
Using this basic pattern, along with other methods, accessors, etc. we can begin to create a task that can do much more for us by allowing the user of the task to provide configuration of the task at runtime.
Calling Dependency Tasks
The last thing I want to show for now, is how to add the ability to call task dependencies when calling our custom task.
In the ‘task :somename’ style of usage, we may want to call other tasks as dependencies of ‘somename’. This is done by using a lambda with the task names. For example:
1: task :somename => [:somedepdency, :anotherdependency, :whatever, :etc] do
2: # do stuff here
3: end
</div> </div>
This code would call the ‘somedependency’, ‘anotherdependency’, ‘whatever’, and ‘etc’ tasks before running the ‘somename’ task.
To provide this same dependency execution in our custom task, we first need to provide a way to specify the dependencies. We can do this by adding a second parameter to the initializer, with a default value that shows we have no dependencies, and then calling the that parameter as a method or hash of methods after storing it in another accessor.
1: class FooTask < Rake::TaskLib
2:
3: attr_accessor :name, :task_dependencies, :data
4:
5: def initialize(name = :test, task_dependencies = {})
6: @name = name
7: yield self if block_given?
8: @task_dependencies = task_dependencies
9: define
10: end
11:
12: def define
13: task name => task_dependencies do
14: puts @data
15: end
16: end
17: end
</div> </div>
Notice the additional ‘task_dependencies’ accessor, the assignment of an empty hash as the default value in the initializer, storage of the parameter in the ‘task_dependencies’ accessor variable, and then the ‘ => task_dependencies’ lambda execution in the define method. This basic code set allows us to specify a number of dependent tasks and have them executed prior to our custom task executing.
To set a dependency for this task, we only need to add the task names as the second parameter to the constructor in our rakefile. Here’s example rakefile with all of our code, a simple task to use as a dependency, and our custom task being called with that dependency.
1: require 'rake'
2: require 'rake/tasklib'
3:
4: class FooTask < Rake::TaskLib
5:
6: attr_accessor :name, :task_dependencies, :data
7:
8: def initialize(name = :test, task_dependencies = {})
9: @name = name
10: yield self if block_given?
11: @task_dependencies = task_dependencies
12: define
13: end
14:
15: def define
16: task name => task_dependencies do
17: puts @data
18: end
19: end
20: end
21:
22: FooTask.new :my_task, :call_me_first do |t|
23: t.data = "I'm using the data accessor!"
24: end
25:
26: task :call_me_first do
27: puts "I'm called first, because I'm the dependency!"
28: end
</div> </div>
The output of this task with its dependency is this:
Moving On
</p> </p> </p> </p> </p>
Now that we have all of the core parts of a real, ‘Rakeish’ custom task in place, we can easily move our FooTask out into it’s own ‘FooTask.rb’ file and have it be a required file in our Rakefile. We could even package this up as a gem and have it be available for installation via the gem system. However, this is not a professional quality task, at this point. There are no tests written for this task, and it doesn’t really do anything valuable other than show how to hack together a custom rake task.
I hope that this little tutorial will at least provide some inspiration to the rest of my Rake using .NET developing colleagues out there in the world, though. I’d really like to see (and will probably build and distribute) a much more professional ‘MSBuild’ rake task, based on this new found knowledge. There are plenty of MSBuild ruby objects out there… it shouldn’t be that difficult to convert it into a rake task and put some unit tests around it.