methadone 1.0.0.rc2 → 1.0.0.rc3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/bin/methadone +2 -2
  2. data/lib/methadone/cucumber.rb +6 -3
  3. data/lib/methadone/error.rb +3 -2
  4. data/lib/methadone/exit_now.rb +25 -0
  5. data/lib/methadone/main.rb +21 -10
  6. data/lib/methadone/sh.rb +19 -4
  7. data/lib/methadone/version.rb +1 -1
  8. data/lib/methadone.rb +1 -0
  9. data/templates/full/Rakefile.erb +1 -1
  10. data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -1
  11. data/test/test_exit_now.rb +37 -0
  12. data/test/test_main.rb +11 -5
  13. data/test/test_sh.rb +17 -0
  14. data/tutorial/.vimrc +6 -0
  15. data/tutorial/1_intro.md +52 -0
  16. data/tutorial/2_bootstrap.md +176 -0
  17. data/tutorial/3_ui.md +349 -0
  18. data/tutorial/4_happy_path.md +437 -0
  19. data/tutorial/5_more_features.md +702 -0
  20. data/tutorial/6_refactor.md +220 -0
  21. data/tutorial/7_logging_debugging.md +304 -0
  22. data/tutorial/8_conclusion.md +11 -0
  23. data/tutorial/code/.rvmrc +1 -0
  24. data/tutorial/code/fullstop/.gitignore +5 -0
  25. data/tutorial/code/fullstop/Gemfile +4 -0
  26. data/tutorial/code/fullstop/LICENSE.txt +202 -0
  27. data/tutorial/code/fullstop/README.rdoc +23 -0
  28. data/tutorial/code/fullstop/Rakefile +31 -0
  29. data/tutorial/code/fullstop/bin/fullstop +43 -0
  30. data/tutorial/code/fullstop/features/fullstop.feature +40 -0
  31. data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +64 -0
  32. data/tutorial/code/fullstop/features/support/env.rb +22 -0
  33. data/tutorial/code/fullstop/fullstop.gemspec +28 -0
  34. data/tutorial/code/fullstop/lib/fullstop/repo.rb +38 -0
  35. data/tutorial/code/fullstop/lib/fullstop/version.rb +3 -0
  36. data/tutorial/code/fullstop/lib/fullstop.rb +2 -0
  37. data/tutorial/code/fullstop/test/tc_something.rb +7 -0
  38. data/tutorial/en.utf-8.add +18 -0
  39. data/tutorial/toc.md +27 -0
  40. metadata +49 -20
@@ -0,0 +1,437 @@
1
+ # The Happy Path
2
+
3
+ We just used TDD and Methadone to create the basics of the user interface for our application. Now, we need to actually
4
+ implement it! Let's focus on the "happy path", i.e. the way the app will work if nothing goes wrong. Since we're doing
5
+ everything test-first, let's write a new scenario for how the app should work.
6
+
7
+ We'll append this to `features/fullstop.feature`:
8
+
9
+ ```cucumber
10
+ Scenario: Happy Path
11
+ Given a git repo with some dotfiles at "/tmp/dotfiles.git"
12
+ When I successfully run "fullstop file:///tmp/dotfiles.git"
13
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
14
+ And the files in "~/dotfiles" should be symlinked in my home directory
15
+ ```
16
+
17
+ Basically, what we're doing is assuming a git repository in `/tmp/dotfiles.git`, which we then expect `fullstop` to clone,
18
+ followed by symlinking the contents to our home directory. There is, however, a slight problem.
19
+
20
+ Suppose we make this scenario pass. This means that *every* time we run this scenario, our dotfiles in our *actual* home
21
+ directory will be blown away. Yikes! We don't want that; we want our test as isolated as it can be. What we'd like is to work
22
+ in a home directory that, from the perspective of our cucumber tests, is not our home directory and completely under the conrol
23
+ of the tests but, from the perspective of the `fullstop` app, is the user's bona-fide home directory.
24
+
25
+ We can easily fake this by changing the environment variable `$HOME` just for the tests. As long as `bin/fullstop` uses this
26
+ environment variable to access the user's home directory (which is perfectly valid), everything will be OK.
27
+
28
+ To do that, we need to modify some of cucumber's plumbing. Methadone won't do this for you, since it's not applicable to every
29
+ situation or app. Open up `features/support/env.rb`. It should look like this:
30
+
31
+ ```ruby
32
+ require 'aruba/cucumber'
33
+ require 'methadone/cucumber'
34
+
35
+ ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
36
+ LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
37
+
38
+ Before do
39
+ # Using "announce" causes massive warnings on 1.9.2
40
+ @puts = true
41
+ @original_rubylib = ENV['RUBYLIB']
42
+ ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
43
+ end
44
+
45
+ After do
46
+ ENV['RUBYLIB'] = @original_rubylib
47
+ end
48
+ ```
49
+
50
+ There's a lot in there already to make our tests work and, fortunately, it makes our job of faking the home directory a bit
51
+ easier. We need to save the original location in `Before`, and then change it there, setting it back to normal in `After`, just
52
+ as we have done with the `$RUBYLIB` environment variable (incidentally, this is how Aruba can run our app without using `bundle
53
+ exec`).
54
+
55
+ ```ruby
56
+ require 'aruba/cucumber'
57
+ require 'methadone/cucumber'
58
+
59
+ ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
60
+ LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
61
+
62
+ Before do
63
+ # Using "announce" causes massive warnings on 1.9.2
64
+ @puts = true
65
+ @original_rubylib = ENV['RUBYLIB']
66
+ ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
67
+ # vvv
68
+ @original_home = ENV['HOME']
69
+ ENV['HOME'] = "/tmp/fakehome"
70
+ FileUtils.rm_rf "/tmp/fakehome"
71
+ FileUtils.mkdir "/tmp/fakehome"
72
+ # ^^^
73
+ end
74
+
75
+ After do
76
+ ENV['RUBYLIB'] = @original_rubylib
77
+ # vvv
78
+ ENV['HOME'] = @original_home
79
+ # ^^^
80
+ end
81
+ ```
82
+
83
+ As you can see, we also delete the directory and re-create it so that anything leftover from a previous test won't cause false
84
+ positives or negatives in our tests.
85
+
86
+ *Now*, let's run our scenario and see where we're at:
87
+
88
+
89
+ ```sh
90
+ $ rake features
91
+ Feature: Checkout dotfiles
92
+ In order to get my dotfiles onto a new computer
93
+ I want a one-command way to keep them up to date
94
+ So I don't have to do it myself
95
+
96
+ Scenario: Basic UI
97
+ When I get help for "fullstop"
98
+ Then the exit status should be 0
99
+ And the banner should be present
100
+ And there should be a one line summary of what the app does
101
+ And the banner should include the version
102
+ And the banner should document that this app takes options
103
+ And the banner should document that this app's arguments are:
104
+ | repo_url | which is required |
105
+
106
+ Scenario: Happy Path
107
+ Given a git repo with some dotfiles at "/tmp/dotfiles.git"
108
+ When I successfully run "fullstop file:///tmp/dotfiles.git"
109
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
110
+ And the files in "~/dotfiles" should be symlinked in my home directory
111
+
112
+ 2 scenarios (1 undefined, 1 passed)
113
+ 11 steps (1 skipped, 3 undefined, 7 passed)
114
+ 0m0.152s
115
+
116
+ You can implement step definitions for undefined steps with these snippets:
117
+
118
+ Given /^a git repo with some dotfiles at "([^"]*)"$/ do |arg1|
119
+ pending # express the regexp above with the code you wish you had
120
+ end
121
+
122
+ Then /^the dotfiles should be checked out in the directory "([^"]*)"$/ do |arg1|
123
+ pending # express the regexp above with the code you wish you had
124
+ end
125
+
126
+ Then /^the files in "([^"]*)" should be symlinked in my home directory$/ do |arg1|
127
+ pending # express the regexp above with the code you wish you had
128
+ end
129
+ ```
130
+
131
+ As you can see there are three steps that cucumber doesn't know how to execute. It provides boilerplate for doing so, so let's
132
+ do that next. We're going to move a bit faster here, since the specifics of implementing cucumber steps is orthogonal to
133
+ Methadone, and we don't want to stray too far from our goal of learning Methadone. If you'd like
134
+ to explore this in more detail, check out the testing chapter of [my book][clibook].
135
+
136
+ [clibook]: http://www.awesomecommandlineapps.com
137
+
138
+ Here's the code to implement these steps, which I've put in `features/step_definitions/fullstop_steps.rb`:
139
+
140
+ ```ruby
141
+ include FileUtils
142
+
143
+ FILES = %w(.vimrc .bashrc .exrc)
144
+
145
+ Given /^a git repo with some dotfiles at "([^"]*)"$/ do |repo_dir|
146
+ @repo_dir = repo_dir
147
+ base_dir = File.dirname(repo_dir)
148
+ dir = File.basename(repo_dir)
149
+ Dir.chdir base_dir do
150
+ rm_rf dir
151
+ mkdir dir
152
+ end
153
+ Dir.chdir repo_dir do
154
+ FILES.each { |_| touch _ }
155
+ sh "git init ."
156
+ sh "git add #{FILES.join(' ')}"
157
+ sh "git commit -a -m 'initial commit'"
158
+ end
159
+ end
160
+
161
+ Then /^the dotfiles should be checked out in the directory "([^"]*)"$/ do |dotfiles_dir|
162
+ # Expand ~ to ENV["HOME"]
163
+ base_dir = File.dirname(dir)
164
+ base_dir = ENV['HOME'] if base_dir == "~"
165
+ dotfiles_dir = File.join(base_dir,File.basename(dotfiles_dir))
166
+
167
+ File.exist?(dotfiles_dir).should == true
168
+ Dir.chdir dotfiles_dir do
169
+ FILES.each do |file|
170
+ File.exist?(file).should == true
171
+ end
172
+ end
173
+ end
174
+
175
+ Then /^the files in "([^"]*)" should be symlinked in my home directory$/ do |dotfiles_dir|
176
+ Dir.chdir(ENV['HOME']) do
177
+ FILES.each do |file|
178
+ File.lstat(file).should be_symlink
179
+ end
180
+ end
181
+ end
182
+ ```
183
+
184
+ In short, we set up a fake git repo in our first step definition, using the `FILES` constant so all steps know which files to expect. In our second step definition, we make sure to expand the "~" into `ENV['HOME']` before checking for the cloned repo. In the last step we check that the files in `ENV['HOME']` are symlinks (being sure to use `lstat` instead of `stat`, as `stat` follows symlinks and will report the files as normal files).
185
+
186
+ You won't be familiar with the method `sh` that we're using to call `git`. This is provided by Methadone and we'll explore it in more detail later. For now, it functions similarly to `system`.
187
+
188
+ We should now have a failing test:
189
+
190
+ ```sh
191
+ $ rake features
192
+ Feature: Checkout dotfiles
193
+ In order to get my dotfiles onto a new computer
194
+ I want a one-command way to keep them up to date
195
+ So I don't have to do it myself
196
+
197
+ Scenario: Basic UI
198
+ When I get help for "fullstop"
199
+ Then the exit status should be 0
200
+ And the banner should be present
201
+ And there should be a one line summary of what the app does
202
+ And the banner should include the version
203
+ And the banner should document that this app takes options
204
+ And the banner should document that this app's arguments are:
205
+ | repo_url | which is required |
206
+
207
+ Scenario: Happy Path
208
+ Given a git repo with some dotfiles at "/tmp/dotfiles.git"
209
+ When I successfully run `fullstop file:///tmp/dotfiles.git`
210
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
211
+ expected: true
212
+ got: false (using ==) (RSpec::Expectations::ExpectationNotMetError)
213
+ ./features/step_definitions/fullstop_steps.rb:29:in `/^the dotfiles should be checked out in the directory "([^"]*)"$/'
214
+ features/fullstop.feature:19:in `Then the dotfiles should be checked out in the directory "~/dotfiles"'
215
+ And the files in "~/dotfiles" should be symlinked in my home directory
216
+
217
+ Failing Scenarios:
218
+ cucumber features/fullstop.feature:16
219
+
220
+ 2 scenarios (1 failed, 1 passed)
221
+ 11 steps (1 failed, 1 skipped, 9 passed)
222
+ 0m0.291s
223
+ rake aborted!
224
+ Cucumber failed
225
+
226
+ Tasks: TOP => features
227
+ (See full trace by running task with --trace)
228
+ ```
229
+
230
+ This is the step that's failing:
231
+
232
+ ```cucumber
233
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
234
+ ```
235
+
236
+ It's a bit hard to understand *why* it's failing, but the error message and line number helps. This is the line that's failing:
237
+
238
+ ```ruby
239
+ File.exist?(dotfiles_dir).should == true
240
+ ```
241
+
242
+ Since `dotfiles_dir` is `~/dotfiles` (or, more specifically, `File.join(ENV['HOME'],'dotfiles')`), and it doesn't exist, since we
243
+ haven't written any code that might cause it to exist, the test fails. Although it's outside the scope of this tutorial, you
244
+ should consider writing some custom RSpec matchers for your assertions, since they can allow you to produce better failure
245
+ messages.
246
+
247
+ Now that we have a failing test, we can start writing some code. This is the first bit of actual logic we'll write, and we need
248
+ to revisit the canonical structure of a Methadone app to know where to put it.
249
+
250
+ Recall that the second part of our app is the "main" block, and it's intended to hold the primary logic of your application. Methadone provides the method `main`, which lives in `Methadone::Main`, and takes a block. This block is where you put your logic. Think of it like the `main` method of a C program.
251
+
252
+ Now that we know where to put our code, we need to know *what* code we need to add. To make this step pass, we need to clone the
253
+ repo given to us on the command-line. To do that we need:
254
+
255
+ * The ability to execute `git`
256
+ * The ability to change to the user's home directory
257
+ * Access to the repo's URL from the command line
258
+
259
+ Although we can use `system` or the backtick operator to call `git`, we're going to use `sh`, which is available by mixing in
260
+ `Methadone::SH`. We'll go into the advantages of why we might want to do that later in the tutorial, but for now, think of it as
261
+ saving us a few characters over `system`.
262
+
263
+ We can change to the user's home directory using the `chdir` method of `Dir`, which is built-in to Ruby. To get the value of the
264
+ URL the user provided on the command-line, we could certainly take it from `ARGV`, but Methadone allows you `main` block to take
265
+ arguments, which it will populate with the contents of `ARGV`. All we need to do is change our `main` block to accept `repo_url`
266
+ as an argument.
267
+
268
+ Here's the code:
269
+
270
+ ```ruby
271
+ #!/usr/bin/env ruby
272
+
273
+ require 'optparse'
274
+ require 'methadone'
275
+ require 'fullstop'
276
+
277
+ class App
278
+ include Methadone::Main
279
+ include Methadone::CLILogging
280
+ # vvv
281
+ include Methadone::SH
282
+ # ^^^
283
+
284
+ # vvv
285
+ main do |repo_url|
286
+ # ^^^
287
+
288
+ # vvv
289
+ Dir.chdir ENV['HOME'] do
290
+ sh "git clone #{repo_url}"
291
+ end
292
+ # ^^^
293
+ end
294
+
295
+ version Fullstop::VERSION
296
+
297
+ description 'Manages dotfiles from a git repo'
298
+
299
+ arg :repo_url, "URL to the git repository containing your dotfiles"
300
+
301
+ use_log_level_option
302
+
303
+ go!
304
+ end
305
+ ```
306
+
307
+ Note that all we're doing here is getting the currently-failing step to pass. We *aren't* implementing the entire app. We want
308
+ to write only the code we need to, and we go one step at a time. Let's re-run our scenario and see if we get farther:
309
+
310
+ ```sh
311
+ rake features
312
+ Feature: Checkout dotfiles
313
+ In order to get my dotfiles onto a new computer
314
+ I want a one-command way to keep them up to date
315
+ So I don't have to do it myself
316
+
317
+ Scenario: Basic UI
318
+ When I get help for "fullstop"
319
+ Then the exit status should be 0
320
+ And the banner should be present
321
+ And there should be a one line summary of what the app does
322
+ And the banner should include the version
323
+ And the banner should document that this app takes options
324
+ And the banner should document that this app's arguments are:
325
+ | repo_url | which is required |
326
+
327
+ Scenario: Happy Path
328
+ Given a git repo with some dotfiles at "/tmp/dotfiles.git"
329
+ When I successfully run `fullstop file:///tmp/dotfiles.git`
330
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
331
+ And the files in "~/dotfiles" should be symlinked in my home directory
332
+ No such file or directory - .vimrc (Errno::ENOENT)
333
+ ./features/step_definitions/fullstop_steps.rb:40:in `lstat'
334
+ ./features/step_definitions/fullstop_steps.rb:40:in `block (3 levels) in <top (required)>'
335
+ ./features/step_definitions/fullstop_steps.rb:39:in `each'
336
+ ./features/step_definitions/fullstop_steps.rb:39:in `block (2 levels) in <top (required)>'
337
+ ./features/step_definitions/fullstop_steps.rb:38:in `chdir'
338
+ ./features/step_definitions/fullstop_steps.rb:38:in `/^the files in "([^"]*)" should be symlinked in my home directory$/'
339
+ features/fullstop.feature:20:in `And the files in "~/dotfiles" should be symlinked in my home directory'
340
+
341
+ Failing Scenarios:
342
+ cucumber features/fullstop.feature:16
343
+
344
+ 2 scenarios (1 failed, 1 passed)
345
+ 11 steps (1 failed, 10 passed)
346
+ 0m0.313s
347
+ rake aborted!
348
+ Cucumber failed
349
+
350
+ Tasks: TOP => features
351
+ (See full trace by running task with --trace)
352
+ ```
353
+
354
+ We're now failing at the next step:
355
+
356
+ ```cucumber
357
+ And the files in "~/dotfiles" should be symlinked in my home directory
358
+ ```
359
+
360
+ The error, "No such file or directory - .vimrc", is being raised from `File.lstat` (as opposed to an explicit test failure).
361
+ This is enough to allow us to write some more code. What we need to do know is iterate over the files in the cloned repo and
362
+ symlink them to the user's home directory. The tools to do this are already available to use via the built-in Ruby library
363
+ `FileUtils`. We'll require it and implement the symlinking logic:
364
+
365
+ ```ruby
366
+ #!/usr/bin/env ruby
367
+
368
+ require 'optparse'
369
+ require 'methadone'
370
+ require 'fullstop'
371
+ # vvv
372
+ require 'fileutils'
373
+ # ^^^
374
+
375
+ class App
376
+ include Methadone::Main
377
+ include Methadone::CLILogging
378
+ include Methadone::SH
379
+
380
+ main do |repo_url|
381
+
382
+ Dir.chdir ENV['HOME'] do
383
+ sh "git clone #{repo_url}"
384
+ basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
385
+ # vvv
386
+ Dir.entries(basedir).each do |file|
387
+ next if file == '.' || file == '..' || file == '.git'
388
+ FileUtils.ln_s file,'.'
389
+ end
390
+ # ^^^
391
+ end
392
+ end
393
+
394
+ version Fullstop::VERSION
395
+
396
+ description 'Manages dotfiles from a git repo'
397
+
398
+ arg :repo_url, "URL to the git repository containing your dotfiles"
399
+
400
+ use_log_level_option
401
+
402
+ go!
403
+ end
404
+ ```
405
+
406
+ Now, let's run our scenario again:
407
+
408
+ ```sh
409
+ $ rake features
410
+ Feature: Checkout dotfiles
411
+ In order to get my dotfiles onto a new computer
412
+ I want a one-command way to keep them up to date
413
+ So I don't have to do it myself
414
+
415
+ Scenario: Basic UI
416
+ When I get help for "fullstop"
417
+ Then the exit status should be 0
418
+ And the banner should be present
419
+ And there should be a one line summary of what the app does
420
+ And the banner should include the version
421
+ And the banner should document that this app takes options
422
+ And the banner should document that this app's arguments are:
423
+ | repo_url | which is required |
424
+
425
+ Scenario: Happy Path
426
+ Given a git repo with some dotfiles at "/tmp/dotfiles.git"
427
+ When I successfully run `fullstop file:///tmp/dotfiles.git`
428
+ Then the dotfiles should be checked out in the directory "~/dotfiles"
429
+ And the files in "~/dotfiles" should be symlinked in my home directory
430
+
431
+ 2 scenarios (2 passed)
432
+ 11 steps (11 passed)
433
+ 0m0.396s
434
+ ```
435
+
436
+ Everything passed! Our app now works for the "happy path". As long as the user starts from a clean home directory, `fullstop`
437
+ will clone their dotfiles, and setup symlinks to them in their home directory. Now that we have the basics of our app running, we'll see how Methadone makes it easy to add new features.