The Tasks
In the previous tutorials we learned how to run a Shoes program from both the development executable bin/shoes
and the gem executable shoes
. In both cases we end up in the command-line interface (CLI) and this line runs the app!
I'd like to dig into a real app, frontend, backend etc but first let's wrap up this section on Shoes executables by talking about the rake tasks.
Rakefile
Let's take a look at the Rakefile.
If you're not familiar with Rakefiles then please take a look at the Rake's README. If you're not familiar with rake/clean
these comments should be enough for our purposes. And lastly you should know that rspec/core/rake_task
pulls in some nice rake tasks from RSpec that we'll be using later on.
OK now, looking on in the file, we see a bunch of files from the tasks
directory getting pulled in. Let's go through those one by one.
Changelog
changelog
is a neat little task that @wasnotrice cooked up for keeping track of changes between versions.
We want Shoes developers to tag every commit with a Changelog
tag (see this wiki page). The purpose is so that at each release we can automatically have a little log of the changes. For an example of what the sum of all these little Changelog
tags outputs take a look at the Changelog that was blogged after pre-release 3.
So how does it work? Well the code starts out in Changelog#generate
which finds the most recently tagged commit (i.e. the last release, take a look at git tag
if, like me, you're new to this) and stores a range string i.e. "v4.0.0.pre3..master"
Sidenote
From this StackOverflow answer I learned that there is a bit more to a commit range
than meets the eye. The upshot is that technically the syntax a..b
means all commits in b
which aren't in a
. Assuming a nice linear set of commits on master
this just amounts to a simple range of commits, but I thought it was interesting that a git
range may be non-linear.
changelog_header
This method generates the header which states how many commits occurred between releases. It makes use of git log
, git rev-list
and git describe
. I'll show how git
commands like these work over in categorize_commits
in detail since that method is a bit more complicated. But the gist of it is that with git
commands we can do something like list out commits, count them, and get simple descriptive names.
categorize_commits
Next categorize_commits
goes through each hash in the CATEGORY_MAPPING
array, and looks for commits (searched for via git log
) that contain a certain :pattern
. The key to understanding this is seeing how log_command
is built here. An example of a fully built command is:
git log --regexp-ignore-case --grep 'Changelog: improvement' --format='%s<--BODY-START-->%b<--BODY-END--> [%h]<--COMMIT-->' v4.0.0.pre3..master
The result is a nicely formatted version of git log
that delineates the body of the message like so:
Merge pull request #1028 from shoes/hidden-style<--BODY-START-->Support the hidden style on all elements
* Changelog: improvement<--BODY-END--> [63afeca]<--COMMIT-->
Merge branch 'master' into move-default-args<--BODY-START-->Move defaulting out of dsl.rb into DSL classes
Fixes #968
Changelog: improvement
<--BODY-END--> [81092b9]<--COMMIT-->
Merge pull request #1032 from shoes/text-block-painter-guard-636<--BODY-START-->Move guard against empty text segments further up in the chain
Changelog: improvement<--BODY-END--> [f121cac]<--COMMIT-->
Merge pull request . . .
After some formatting touches supplied by uniform_change_log
we get two outputs: the commits which are put into categorized_commits
which look like this for my sample:
["* Support the hidden style on all elements\r [63afeca]", "* Merge branch 'master' into move-default-args [81092b9]", "* Move guard against empty text segments further up in the chain\r [f121cac]"
and change
which is a formatted version produced by changes_under_heading
that looks like this for the improvement changes I'm demonstrating
[nil, "Improvements (5)\n----------------\n\n* Support the hidden style on all elements\r [63afeca]\n* Merge branch 'master' into move-default-args [81092b9]\n* Move guard against empty text segments further up in the chain\r [f121cac]\n . . .
Lastly the categorized_commits
method hands back change
after adding one more category "Miscellaneous" which is the difference between the categorized_commits
array and the changes
variable, just a catch all for leftover changelog entries.
Contributors
Next the contributors are extracted via git shortlog
. Here's an example input and output
input
git shortlog --numbered --summary v4.0.0.pre3..master
output
48 Jason R. Clark
19 Tobias Pfeiffer
12 KC Erb
6 Eric Watson
5 Thomas Graves
2 David English
1 bx10000
This output gets put under a new heading via the same heading method from before and before the string is handed back it is compact
ed (to remove nil
s) and joined.
And that's it for the changelog
. I know its kind of a quick run through but hopefully its enough to get you familiar with the basic ideas.
Console
console
is a little rake task just for running a pry session with Shoes loaded. Such a great thing! If you don't know why that is great then you might not know what pry
is and should check it out! Also you should watch this great RailsConf talk on DDD - Debugger Driven Development. Very cool things!
Gem
gem
is a set of tasks for building and releasing gems.
Sidenote: When I first came across this file, I'd never built or released a gem before and had a lot of questions. So I'm going to jot down the things I learned here in a bit more detail than the average ruby developer would need. The source for much of my commentary is this discussion I had with other cordwainers.
install build release
The first line of gem.rb
pulls in some standard rake tasks that bundler provides: install
, build
, and release
. You can find the definition of these tasks here. install
installs a built gem and release
puts the built gem up on rubygems so the important question is: what does build
do?
I like the way @PragTob put it:
Build takes the gemspec (or
build:all
takes all gemspecs) and packages up the gems according to them, i.e. it produces a.gem
file that contains all the files - so that file is an archive, zip or something. This file can then be distributed. You can try it! runrake build:all
and it should create a bunch of files in thepkg
directory. You could then dogem install pkg/gem_name.gem
to install the gem.
empty tasks
The next few lines contain task declarations with no blocks attached. For example:
desc 'Build all gems'
task 'build:all'
These tasks are getting initialized so that we can attach other tasks to them.
shoes
is a meta-gem, it's just a container for the actual Shoes gems shoes-core
etc. So we attach the actual work of building, installing etc. of each gem to an encompassing build:all
. That's what the each
loop in the next few lines is doing.
The following lines handle the pure shoes
meta-gem. This one gets special attention because the default rake build
task (and others) is already set-up to work as expected, so there's no need to cd
into the directory and then run rake build
.
One more thing about those empty tasks. The correct way to describe a line like this:
task "build:all" => "build:shoes"
is
when
build:shoes
gets invoked (in Rake's terminology), then before you execute thebuild:shoes
task, make sure that thebuild
task has been run. Then runbuild:shoes
.
Thanks to @wasnotrice for explaining this to me!
update_versions
Lastly, each of the gems has a version.rb
file that looks like this:
class Shoes
VERSION = "4.0.0.pre3"
end
it would be a hassle to update all of these by hand, so this task will update these based on the root VERSION
file.
OK, so that does it for the gem
tasks.
Sample
sample
provides some tasks under the namespace samples
. If like me you're not familiar with Rake's namespace
feature, how about you skip down and take a look at the last three lines. Defining tasks like samples:random
is made possible by defining them in the namespace
block.
In this file, the block gathers up all the samples from the SAMPLES_DIR
and puts them into two categories: those that are listed as working in samples/README
and those that aren't. Depending on which ones you want to run (go ahead and look around at all the options), the code here calls the samples one at a time via . . .
def run_sample(sample_name, index, total)
puts "Running #{sample_name} (#{index + 1} of #{total})...quit to run next sample"
system "bin/shoes #{sample_name}"
end
yup, bin/shoes
.
Yard
And last but not least (before moving onto rspec that is) the yard task uses the yard
gem to auto-generate documentation. To my knowledge we're not actively using yard-based documentation so I'm not going to dig into an "all about yard" aside here. Let me know if you'd rather I did though :)
RSpec
If you're totally new to RSpec go on over to the website and take a look. (Also, I would recommend checking out the specs section of the README if you haven't seen it.) The purpose of this task is to get our tests up and running, but it's actually a pretty complicated job. Let's dig in and find out why.
A little about JRuby and Rake
The first thing we do is set the runRubyInProcess
configuration variable of the currently running JRuby runtime to false
. We do this because we're going to launch the specs in a sub-jruby and if we don't, the current VM will ignore arguments given to the second one, and we will definitely want to be able to pass arguments in! For (a little) more info see this JRuby wiki page.
After some method definitions, we define the default task (invoked by $ rake
) to be this one (on line 47). Then we do something new: pass in [:module] => "spec:all"
as an argument to the task method. This syntax let's us pass arguments into the spec. This is what lets us just run the Shape
specs as described in the README. To get a feel for this try running the following file named Rakefile
:
task default: :spec
task :spec, [:module] => "spec:all"
namespace :spec do
task "all", [:module] do |t, args|
puts t
puts args
end
end
by $ rake spec:all[Shape] --trace
you should get back:
** Invoke spec:all (first_time)
** Execute spec:all
spec:all
{:module=>"Shape"}
We'll be using this kind of syntax a lot here so it's good to keep in mind what's going on. The all
task of spec
, for example, invokes each of the shoes
, swt
, and package
specs and passes to each one a String
identifying the DSL element that's to be tested.
The spec
namespace fundamentally has 3 tasks: swt
(which has 3 of it's own subtasks), core
(which is aliased as both shoes
and dsl
), and package
.
Integration and Isolation, Frontend and Backend
One of the key things to know about Shoes is that it is a DSL for writing GUIs. The DSL is small and powerful and as a cordwainer it should be your bread and butter. The goal in creating Shoes is to implement that DSL on a bunch of platforms: Windows, Linux and Mac OS X to name a few. The pure DSL elements (Flow
, Stack
, Button
, etc.) with no actual implementation are the frontend. The actual nuts and bolts that convert syntax like para "Hi"
into text on the screen is the backend (which is sometimes called the gui). Right now Shoes development is focused on one backend: SWT. But we'd like to support many backends. For example someday there may be a backend for mobile devices.
Both the shoes-core
and shoes-swt
directories contain a set of specs. The shoes-core
specs are supposed to spec out the frontend, and the shoes-swt
specs are supposed to spec out the SWT backend. You might think we would just call each of these specs in isolation, but the trouble is the front-end needs a backend. So it can be called with either the Mock backend, or the SWT backend. The Mock backend is meant to be minimal so that the shoes-core
specs are in isolation when they run, but it's important to remember that these are still specs that integrate the DSL (frontend) with a backend.
What this boils down to is that the frontend specs can be run two ways: with the Mock backend or the SWT backend. The backend specs can only be run one way. Therefore we call the frontend specs integration-specs and the backend specs isolation-specs.
The :swt namespace
The :swt
namespace can run the frontend (isolation) specs, the backend (integration) specs, or both. Hence the three tasks: all
, isolation
and integration
.
All three of these specs run essentially the same way:
swt_args(args)
- stick all of the appropriate file paths into an array
jruby_rspec(files, args)
So let's take a look at the first and last methods defined above
First swt_args
sets up a hash of arguments to be passed to the specs called argh
. If an argument was passed in, it is attached to the :module
key. Here's an example argh
produced on my system by running
$ rake spec:swt[Shape]
argh
will be
{:module=>"Shape", :swt=>true, :require=>"shoes-swt/spec/spec_helper", :excludes=>[:no_swt, :fails_on_osx]}
Next, jruby_rspec
takes the :swt
key and creates an rspec_opts
from the remaining keys with appropriate mappings (such as :require
to -r
) the resultant rspec_opts
output from the hash above is:
-e ::Shape -rshoes-swt/spec/spec_helper --tag ~no_swt --tag ~fails_on_osx
Next this gets combined with the files array into an RSpec command via the rspec
method. The output is
rspec --tty -e ::Shape -rshoes-swt/spec/spec_helper --tag ~no_swt --tag ~fails_on_osx
shoes-swt/spec/shoes/cli_spec.rb
shoes-swt/spec/shoes/swt/animation_spec.rb
shoes-swt/spec/shoes/swt/app_spec.rb
shoes-swt/spec/shoes/swt/arc_spec.rb
...
a/bunch/more/files
Finally jruby_run
runs the command with a new jruby
instance passing in the command (and the start on first thread option if applicable).
spec:swt:integration vs spec:core
The primary difference between the way the swt
specs run the shoes-core
specs and the way core
runs them is which spec_helper
file gets included. swt
does this while core
does this. After that the rest is the same. The next tutorial will dig into what these spec_helper
files do and how they specify which backend to test the frontend with, so I'll leave off on that for now.
Overview
So we took a quick spin through a lot of code that supports getting Shoes up and running in a development environment, with an app given, and for testing.
Since these tutorials are for Shoes developers and Shoes is meant to be some kind of TDD / BDD / DDD, the next set of tutorials will walk through the Shoes frontend (core) class by class, by looking at how these specs describe it.