methadone 1.0.0.rc5 → 1.0.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/lib/methadone/cli.rb +2 -0
  2. data/lib/methadone/cli_logging.rb +3 -0
  3. data/lib/methadone/cucumber.rb +1 -1
  4. data/lib/methadone/error.rb +8 -1
  5. data/lib/methadone/execution_strategy/jvm.rb +2 -0
  6. data/lib/methadone/execution_strategy/mri.rb +2 -0
  7. data/lib/methadone/execution_strategy/open_3.rb +2 -0
  8. data/lib/methadone/execution_strategy/open_4.rb +2 -0
  9. data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
  10. data/lib/methadone/exit_now.rb +18 -3
  11. data/lib/methadone/main.rb +34 -6
  12. data/lib/methadone/process_status.rb +45 -0
  13. data/lib/methadone/sh.rb +52 -29
  14. data/lib/methadone/version.rb +1 -1
  15. data/methadone.gemspec +10 -0
  16. data/test/test_main.rb +20 -0
  17. data/test/test_sh.rb +104 -3
  18. metadata +23 -47
  19. data/tutorial/.vimrc +0 -6
  20. data/tutorial/1_intro.md +0 -52
  21. data/tutorial/2_bootstrap.md +0 -174
  22. data/tutorial/3_ui.md +0 -336
  23. data/tutorial/4_happy_path.md +0 -405
  24. data/tutorial/5_more_features.md +0 -693
  25. data/tutorial/6_refactor.md +0 -220
  26. data/tutorial/7_logging_debugging.md +0 -274
  27. data/tutorial/8_conclusion.md +0 -11
  28. data/tutorial/code/.rvmrc +0 -1
  29. data/tutorial/code/fullstop/.gitignore +0 -5
  30. data/tutorial/code/fullstop/Gemfile +0 -4
  31. data/tutorial/code/fullstop/LICENSE.txt +0 -202
  32. data/tutorial/code/fullstop/README.rdoc +0 -23
  33. data/tutorial/code/fullstop/Rakefile +0 -31
  34. data/tutorial/code/fullstop/bin/fullstop +0 -43
  35. data/tutorial/code/fullstop/features/fullstop.feature +0 -40
  36. data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
  37. data/tutorial/code/fullstop/features/support/env.rb +0 -22
  38. data/tutorial/code/fullstop/fullstop.gemspec +0 -28
  39. data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
  40. data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
  41. data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
  42. data/tutorial/code/fullstop/test/tc_something.rb +0 -7
  43. data/tutorial/en.utf-8.add +0 -18
  44. data/tutorial/toc.md +0 -27
data/tutorial/3_ui.md DELETED
@@ -1,336 +0,0 @@
1
- # Tutorial: UI
2
-
3
- We're taking an [outside-in][outsidein] approach to our app. This means that we start with the user interface, and work our way
4
- down to make that interface a reality. I highly recommend this approach, since it forces you to focus on the user of your app
5
- first. Doing this will result in an app that's easier to use, and thus easier for you to maintain and enhance over time.
6
-
7
- Before we dive into our Cucumber features, let's take a moment to think about how our app might work. Essentially, we want it to
8
- clone a git repository somewhere on disk, and then symlink all of those files and directories in the top level of that repo into
9
- our home directory. If we did this in `bash` on the command line, it might be something like this:
10
-
11
- ```sh
12
- $ cd ~
13
- $ git clone git@github.com:davetron5000/dotfiles.git
14
- $ for file in `ls -a dotfiles`; do
15
- > ln -s dotfiles/$file .
16
- > done
17
- ```
18
-
19
- It's worth understanding why we don't just make this entire thing a `bash` script. Our app is going to need more smarts than the
20
- above code: for example, it will need to be able to check if the repo is cloned already, and update it instead of cloning. It
21
- will need good error handling so the user knows if they did something wrong. It might need more complex logic as we use the app
22
- over time. Implementing these in `bash` is painful. `bash` is not a very powerful language, and we'll quickly hit a wall.
23
- Although `bash` is "close to the metal", we'll see that Ruby + Methadone can provide a *very* similar programming experience.
24
-
25
- Now, let's think about how our app will work. We can summarize our app's interface as having two main features at this point:
26
-
27
- * It should accept a required argument that is the URL of the repo to clone
28
- * It should otherwise be a well-behaved and polished command-line app:
29
- * It should have online help.
30
- * Getting help is not an error and the app should exit zero when you get help.
31
- * There should be a usage statement for the app's invocation syntax.
32
- * The app should document what it does.
33
- * The app should indicate what options and arguments it takes.
34
-
35
- Aruba and Methadone provide all the steps we need to test for these aspects of our app's user interface. Here's the feature
36
- we'll use to test this. We can replace the contents of `features/fullstop.feature` with this:
37
-
38
- ```cucumber
39
- Feature: Checkout dotfiles
40
- In order to get my dotfiles onto a new computer
41
- I want a one-command way to keep them up to date
42
- So I don't have to do it myself
43
-
44
- Scenario: Basic UI
45
- When I get help for "fullstop"
46
- Then the exit status should be 0
47
- And the banner should be present
48
- And there should be a one line summary of what the app does
49
- And the banner should include the version
50
- And the banner should document that this app takes options
51
- And the banner should document that this app's arguments are:
52
- |repo_url|which is required|
53
- ```
54
-
55
- This scenario describes getting help for our app and the basic user interface that we need. This is how we "test-drive" the
56
- development of the user interface portion of our app. Let's run the scenario and see what happens.
57
-
58
- ```sh
59
- $ rake features
60
- Feature: Checkout dotfiles
61
- In order to get my dotfiles onto a new computer
62
- I want a one-command way to keep them up to date
63
- So I don't have to do it myself
64
-
65
- Scenario: Basic UI
66
- When I get help for "fullstop"
67
- Then the exit status should be 0
68
- And the banner should be present
69
- And there should be a one line summary of what the app does
70
- expected "v0.0.1" to match /^\w\w+\s+\w\w+/ (RSpec::Expectations::ExpectationNotMetError)
71
- features/fullstop.feature:10:in `And there should be a one line summary of what the app does'
72
- And the banner should include the version
73
- And the banner should document that this app takes options
74
- And the banner should document that this app's arguments are:
75
- | repo_url | which is required |
76
-
77
- Failing Scenarios:
78
- cucumber features/fullstop.feature:6
79
-
80
- 1 scenario (1 failed)
81
- 7 steps (1 failed, 3 skipped, 3 passed)
82
- 0m0.126s
83
- rake aborted!
84
- Cucumber failed
85
-
86
- Tasks: TOP => features
87
- (See full trace by running task with --trace)
88
- ```
89
-
90
- We have a failing test! Note that cucumber knows about all of these steps; between Aruba and Metahdone, they are all already defined. We'll see some custom steps later, that are specific to our app, but for now, we haven't had to write any testing code, which is great!
91
-
92
- You'll also notice that some steps are already passing, despite the fact that we've done no coding. Also notice that cucumber didn't complain about unknown steps. Methadone provides almost all of these cucumber steps for us. The rest are provided by Aruba. Since Methadone generated an executable for us when we ran the `methadone` command, it already provides the ability to get help, and exits with the correct exit status.
93
-
94
- Let's fix things one step at a time, so we can see exactly what we need to do. The current scenario is failing because our app doesn't have a one line summary. This summary is important so that we can remember what the app does later on (despite how clever our name is, it's likely we'll forget a few months from now and the description will jog our memory).
95
-
96
- Let's have a look at our executable. A Methadone app is made up of four parts: the setup where we require necessary libraries, a "main" block containing the primary logic of our code, a block of code that declares the app's UI, and a call to `go!`, which runs our app.
97
-
98
- ```ruby
99
- #!/usr/bin/env ruby
100
-
101
- # setup
102
- require 'optparse'
103
- require 'methadone'
104
- require 'fullstop'
105
-
106
- class App
107
- include Methadone::Main
108
- include Methadone::CLILogging
109
-
110
- # the main block
111
- main do
112
-
113
- end
114
-
115
- # declare UI
116
- version Fullstop::VERSION
117
-
118
- use_log_level_option
119
-
120
- # call go!
121
- go!
122
- end
123
- ```
124
-
125
- There's not much magic going on here; you could think of this code as being roughly equivalent to:
126
-
127
- ```ruby
128
- def main
129
- end
130
-
131
- opts = OptionParser.new
132
- opts.banner = "usage: $0 [options]\n\nversion: #{Fullstop::VERSION}"
133
- opts.parse!
134
-
135
- main
136
- ```
137
-
138
- We'll see later that Methadone does a lot more than this, but this should help you understand the control flow. We now need to
139
- add a one-line description for our app.
140
-
141
- In a vanilla Ruby application, we'd use the `banner` method of an `OptionParser` to add this description (much as we do with the
142
- version in the above, non-Methadone code). Methadone actually manages an
143
- `OptionParser` instance that we can use, available via the `opts` method. We could call `banner` on that to set our description, but Methadone provides a convenience method to do it for us: `description`.
144
-
145
- `description` takes a single argument: a one-line description of our app. Methadone will then include this in the online help
146
- output of our app when the user uses `-h` or `--help`. We'll add a call to it in the "declare UI" portion of our code:
147
-
148
- ```ruby
149
- #!/usr/bin/env ruby
150
-
151
- require 'optparse'
152
- require 'methadone'
153
- require 'fullstop'
154
-
155
- class App
156
- include Methadone::Main
157
- include Methadone::CLILogging
158
-
159
- main do
160
- end
161
-
162
- version Fullstop::VERSION
163
-
164
- # vvv
165
- description 'Manages dotfiles from a git repo'
166
- # ^^^
167
-
168
- use_log_level_option
169
-
170
- go!
171
- end
172
- ```
173
-
174
- Now, let's run our scenario again:
175
-
176
- ```sh
177
- $ rake features
178
- Feature: Checkout dotfiles
179
- In order to get my dotfiles onto a new computer
180
- I want a one-command way to keep them up to date
181
- So I don't have to do it myself
182
-
183
- Scenario: Basic UI
184
- When I get help for "fullstop"
185
- Then the exit status should be 0
186
- And the banner should be present
187
- And there should be a one line summary of what the app does
188
- And the banner should include the version
189
- And the banner should document that this app takes options
190
- And the banner should document that this app's arguments are:
191
- | repo_url | which is required |
192
- expected "Usage: fullstop [options]\n\nManages dotfiles from a git repo\n\nv0.0.1\n\nOptions:\n --version Show help/version info\n --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)\n (Default: info)\n" to include "repo_url"
193
- Diff:
194
- @@ -1,2 +1,11 @@
195
- -["repo_url"]
196
- +Usage: fullstop [options]
197
- +
198
- +Manages dotfiles from a git repo
199
- +
200
- +v0.0.1
201
- +
202
- +Options:
203
- + --version Show help/version info
204
- + --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
205
- + (Default: info)
206
- (RSpec::Expectations::ExpectationNotMetError)
207
- features/fullstop.feature:13:in `And the banner should document that this app's arguments are:'
208
-
209
- Failing Scenarios:
210
- cucumber features/fullstop.feature:6
211
-
212
- 1 scenario (1 failed)
213
- 7 steps (1 failed, 6 passed)
214
- 0m0.132s
215
- rake aborted!
216
- Cucumber failed
217
-
218
- Tasks: TOP => features
219
- (See full trace by running task with --trace)
220
- ```
221
-
222
- We got farther this time. Our step for checking that we have a one-line summary is passing. Further, the next two following steps are also passing, despite the fact that we did nothing to explicitly make them pass. Like the preceding steps ("Then the exit status should be 0" and "And the banner should be present"), the two steps following the one we just fixed pass because Methadone has bootstrapped our app in a way that they are already passing.
223
-
224
- The call to Methadone's `version` method ensures that the version of our app appears in the online help. The other step, "And the banner should document that this app takes options" passes because we are allowing Methadone to manage the banner. Methadone knows that our app takes options (namely `--version`), and inserts the string `"[options]"` into the usage statement.
225
-
226
- The last step in our scenario is still failing, so let's fix that to finish up our user interface. What Methadone is looking for is for the string `repo_url` (the name of our only, required, argument) to be in the usage string, in other words, Methadone is expecting to see this:
227
-
228
- ```
229
- Usage: fullstop [option] repo_url
230
- ```
231
-
232
- Right now, our app's usage string looks like this:
233
-
234
- ```
235
- Usage: fullstop [option]
236
- ```
237
-
238
- Again, if we were using `OptionParser`, we would need to modify the argument given to `banner` to include this string. Methadone provides a method, `arg` that will do this automatically for us. We'll add it right after the call to `description` in the "declare UI" section of our app:
239
-
240
- ```ruby
241
- #!/usr/bin/env ruby
242
-
243
- require 'optparse'
244
- require 'methadone'
245
- require 'fullstop'
246
-
247
- class App
248
- include Methadone::Main
249
- include Methadone::CLILogging
250
-
251
- main do
252
- end
253
-
254
- version Fullstop::VERSION
255
-
256
- description 'Manages dotfiles from a git repo'
257
-
258
- # vvv
259
- arg :repo_url, "URL to the git repository containing your dotfiles"
260
- # ^^^
261
-
262
- use_log_level_option
263
-
264
- go!
265
- end
266
- ```
267
-
268
- Now, when we run our features again, we can see that everything passes:
269
-
270
- ```sh
271
- $ rake features
272
- Feature: Checkout dotfiles
273
- In order to get my dotfiles onto a new computer
274
- I want a one-command way to keep them up to date
275
- So I don't have to do it myself
276
-
277
- Scenario: Basic UI
278
- When I get help for "fullstop"
279
- Then the exit status should be 0
280
- And the banner should be present
281
- And there should be a one line summary of what the app does
282
- And the banner should include the version
283
- And the banner should document that this app takes options
284
- And the banner should document that this app's arguments are:
285
- | repo_url | which is required |
286
-
287
- 1 scenario (1 passed)
288
- 7 steps (7 passed)
289
- 0m0.129s
290
- ```
291
-
292
- Nice! If our UI should ever change, we'll notice the regression, and we also have an easy way to use TDD to enhance our
293
- application's UI in the future. Let's take a look at it ourselves to see what it's like:
294
-
295
- ```sh
296
- $ bundle exec bin/fullstop --help
297
- Usage: fullstop [options] repo_url
298
-
299
- Manages dotfiles from a git repo
300
-
301
- v0.0.1
302
-
303
- Options:
304
- --version Show help/version info
305
- --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
306
- (Default: info)
307
- ```
308
-
309
- Not to bad for having written two lines of code! We can also see that `fullstop` will error out if we omit our required
310
- argument, `repo_url`:
311
-
312
- ```sh
313
- $ bundle exec bin/fullstop
314
- parse error: 'repo_url' is required
315
-
316
- Usage: fullstop [options] repo_url
317
-
318
- Manages dotfiles from a git repo
319
-
320
- v0.0.1
321
-
322
- Options:
323
- --version Show help/version info
324
- --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
325
- (Default: info)
326
- $ echo $?
327
- 64
328
- ```
329
-
330
- We see an error message, and exited nonzero (64 is a somewhat standard exit code for errors in command-line invocation).
331
-
332
- It's also worth pointing out that Methadone is taking a very light touch. We could completely re-implement `bin/fullstop` using `OptionParser` and still have our scenario pass. As we'll see, few of Methadone's parts really rely on each other, and many can be used piecemeal, if that's what you want.
333
-
334
- Now that we have our UI, the next order of business is to actually implement something.
335
-
336
- [outsidein]: http://en.wikipedia.org/wiki/Outside%E2%80%93in_software_development
@@ -1,405 +0,0 @@
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, followed by symlinking the contents to our home directory. There is, however, a slight problem.
18
-
19
- Suppose we make this scenario pass. This means that *every* time we run this scenario, our dotfiles in our *actual* home 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 in a home directory that, from the perspective of our cucumber tests, is not our home directory and completely under the conrol of the tests but, from the perspective of the `fullstop` app, is the user's bona-fide home directory.
20
-
21
- We can easily fake this by changing the environment variable `$HOME` just for the tests. As long as `bin/fullstop` uses this environment variable to access the user's home directory (which is perfectly valid), everything will be OK.
22
-
23
- 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 situation or app. Open up `features/support/env.rb`. It should look like this:
24
-
25
- ```ruby
26
- require 'aruba/cucumber'
27
- require 'methadone/cucumber'
28
-
29
- ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
30
- LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
31
-
32
- Before do
33
- # Using "announce" causes massive warnings on 1.9.2
34
- @puts = true
35
- @original_rubylib = ENV['RUBYLIB']
36
- ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
37
- end
38
-
39
- After do
40
- ENV['RUBYLIB'] = @original_rubylib
41
- end
42
- ```
43
-
44
- 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 easier. We need to save the original location in `Before`, and then change it there, setting it back to normal in `After`, just as we have done with the `$RUBYLIB` environment variable (incidentally, this is how Aruba can run our app without using `bundle exec`).
45
-
46
- ```ruby
47
- require 'aruba/cucumber'
48
- require 'methadone/cucumber'
49
-
50
- ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
51
- LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
52
-
53
- Before do
54
- # Using "announce" causes massive warnings on 1.9.2
55
- @puts = true
56
- @original_rubylib = ENV['RUBYLIB']
57
- ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
58
- # vvv
59
- @original_home = ENV['HOME']
60
- ENV['HOME'] = "/tmp/fakehome"
61
- FileUtils.rm_rf "/tmp/fakehome"
62
- FileUtils.mkdir "/tmp/fakehome"
63
- # ^^^
64
- end
65
-
66
- After do
67
- ENV['RUBYLIB'] = @original_rubylib
68
- # vvv
69
- ENV['HOME'] = @original_home
70
- # ^^^
71
- end
72
- ```
73
-
74
- 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
75
- positives or negatives in our tests.
76
-
77
- *Now*, let's run our scenario and see where we're at:
78
-
79
-
80
- ```sh
81
- $ rake features
82
- Feature: Checkout dotfiles
83
- In order to get my dotfiles onto a new computer
84
- I want a one-command way to keep them up to date
85
- So I don't have to do it myself
86
-
87
- Scenario: Basic UI
88
- When I get help for "fullstop"
89
- Then the exit status should be 0
90
- And the banner should be present
91
- And there should be a one line summary of what the app does
92
- And the banner should include the version
93
- And the banner should document that this app takes options
94
- And the banner should document that this app's arguments are:
95
- | repo_url | which is required |
96
-
97
- Scenario: Happy Path
98
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
99
- When I successfully run "fullstop file:///tmp/dotfiles.git"
100
- Then the dotfiles should be checked out in the directory "~/dotfiles"
101
- And the files in "~/dotfiles" should be symlinked in my home directory
102
-
103
- 2 scenarios (1 undefined, 1 passed)
104
- 11 steps (1 skipped, 3 undefined, 7 passed)
105
- 0m0.152s
106
-
107
- You can implement step definitions for undefined steps with these snippets:
108
-
109
- Given /^a git repo with some dotfiles at "([^"]*)"$/ do |arg1|
110
- pending # express the regexp above with the code you wish you had
111
- end
112
-
113
- Then /^the dotfiles should be checked out in the directory "([^"]*)"$/ do |arg1|
114
- pending # express the regexp above with the code you wish you had
115
- end
116
-
117
- Then /^the files in "([^"]*)" should be symlinked in my home directory$/ do |arg1|
118
- pending # express the regexp above with the code you wish you had
119
- end
120
- ```
121
-
122
- 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 do that next. We're going to move a bit faster here, since the specifics of implementing cucumber steps is orthogonal to Methadone, and we don't want to stray too far from our goal of learning Methadone. If you'd like to explore this in more detail, check out the testing chapter of [my book][clibook].
123
-
124
- [clibook]: http://www.awesomecommandlineapps.com
125
-
126
- Here's the code to implement these steps, which I've put in `features/step_definitions/fullstop_steps.rb`:
127
-
128
- ```ruby
129
- include FileUtils
130
-
131
- FILES = %w(.vimrc .bashrc .exrc)
132
-
133
- Given /^a git repo with some dotfiles at "([^"]*)"$/ do |repo_dir|
134
- @repo_dir = repo_dir
135
- base_dir = File.dirname(repo_dir)
136
- dir = File.basename(repo_dir)
137
- Dir.chdir base_dir do
138
- rm_rf dir
139
- mkdir dir
140
- end
141
- Dir.chdir repo_dir do
142
- FILES.each { |_| touch _ }
143
- sh "git init ."
144
- sh "git add #{FILES.join(' ')}"
145
- sh "git commit -a -m 'initial commit'"
146
- end
147
- end
148
-
149
- Then /^the dotfiles should be checked out in the directory "([^"]*)"$/ do |dotfiles_dir|
150
- # Expand ~ to ENV["HOME"]
151
- base_dir = File.dirname(dir)
152
- base_dir = ENV['HOME'] if base_dir == "~"
153
- dotfiles_dir = File.join(base_dir,File.basename(dotfiles_dir))
154
-
155
- File.exist?(dotfiles_dir).should == true
156
- Dir.chdir dotfiles_dir do
157
- FILES.each do |file|
158
- File.exist?(file).should == true
159
- end
160
- end
161
- end
162
-
163
- Then /^the files in "([^"]*)" should be symlinked in my home directory$/ do |dotfiles_dir|
164
- Dir.chdir(ENV['HOME']) do
165
- FILES.each do |file|
166
- File.lstat(file).should be_symlink
167
- end
168
- end
169
- end
170
- ```
171
-
172
- 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).
173
-
174
- 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`.
175
-
176
- We should now have a failing test:
177
-
178
- ```sh
179
- $ rake features
180
- Feature: Checkout dotfiles
181
- In order to get my dotfiles onto a new computer
182
- I want a one-command way to keep them up to date
183
- So I don't have to do it myself
184
-
185
- Scenario: Basic UI
186
- When I get help for "fullstop"
187
- Then the exit status should be 0
188
- And the banner should be present
189
- And there should be a one line summary of what the app does
190
- And the banner should include the version
191
- And the banner should document that this app takes options
192
- And the banner should document that this app's arguments are:
193
- | repo_url | which is required |
194
-
195
- Scenario: Happy Path
196
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
197
- When I successfully run `fullstop file:///tmp/dotfiles.git`
198
- Then the dotfiles should be checked out in the directory "~/dotfiles"
199
- expected: true
200
- got: false (using ==) (RSpec::Expectations::ExpectationNotMetError)
201
- ./features/step_definitions/fullstop_steps.rb:29:in `/^the dotfiles should be checked out in the directory "([^"]*)"$/'
202
- features/fullstop.feature:19:in `Then the dotfiles should be checked out in the directory "~/dotfiles"'
203
- And the files in "~/dotfiles" should be symlinked in my home directory
204
-
205
- Failing Scenarios:
206
- cucumber features/fullstop.feature:16
207
-
208
- 2 scenarios (1 failed, 1 passed)
209
- 11 steps (1 failed, 1 skipped, 9 passed)
210
- 0m0.291s
211
- rake aborted!
212
- Cucumber failed
213
-
214
- Tasks: TOP => features
215
- (See full trace by running task with --trace)
216
- ```
217
-
218
- This is the step that's failing:
219
-
220
- ```cucumber
221
- Then the dotfiles should be checked out in the directory "~/dotfiles"
222
- ```
223
-
224
- 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:
225
-
226
- ```ruby
227
- File.exist?(dotfiles_dir).should == true
228
- ```
229
-
230
- Since `dotfiles_dir` is `~/dotfiles` (or, more specifically, `File.join(ENV['HOME'],'dotfiles')`), and it doesn't exist, since we haven't written any code that might cause it to exist, the test fails. Although it's outside the scope of this tutorial, you should consider writing some custom RSpec matchers for your assertions, since they can allow you to produce better failure messages.
231
- 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 to revisit the canonical structure of a Methadone app to know where to put it.
232
- 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.
233
- 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 repo given to us on the command-line. To do that we need:
234
- * The ability to execute `git`
235
- * The ability to change to the user's home directory
236
- * Access to the repo's URL from the command line
237
- Although we can use `system` or the backtick operator to call `git`, we're going to use `sh`, which is available by mixing in `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 saving us a few characters over `system`.
238
-
239
- 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 URL the user provided on the command-line, we could certainly take it from `ARGV`, but Methadone allows you `main` block to take arguments, which it will populate with the contents of `ARGV`. All we need to do is change our `main` block to accept `repo_url` as an argument.
240
-
241
- Here's the code:
242
-
243
- ```ruby
244
- #!/usr/bin/env ruby
245
-
246
- require 'optparse'
247
- require 'methadone'
248
- require 'fullstop'
249
-
250
- class App
251
- include Methadone::Main
252
- include Methadone::CLILogging
253
- # vvv
254
- include Methadone::SH
255
- # ^^^
256
-
257
- # vvv
258
- main do |repo_url|
259
- # ^^^
260
-
261
- # vvv
262
- Dir.chdir ENV['HOME'] do
263
- sh "git clone #{repo_url}"
264
- end
265
- # ^^^
266
- end
267
-
268
- version Fullstop::VERSION
269
-
270
- description 'Manages dotfiles from a git repo'
271
-
272
- arg :repo_url, "URL to the git repository containing your dotfiles"
273
-
274
- use_log_level_option
275
-
276
- go!
277
- end
278
- ```
279
-
280
- Note that all we're doing here is getting the currently-failing step to pass. We *aren't* implementing the entire app. We want 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:
281
-
282
- ```sh
283
- rake features
284
- Feature: Checkout dotfiles
285
- In order to get my dotfiles onto a new computer
286
- I want a one-command way to keep them up to date
287
- So I don't have to do it myself
288
-
289
- Scenario: Basic UI
290
- When I get help for "fullstop"
291
- Then the exit status should be 0
292
- And the banner should be present
293
- And there should be a one line summary of what the app does
294
- And the banner should include the version
295
- And the banner should document that this app takes options
296
- And the banner should document that this app's arguments are:
297
- | repo_url | which is required |
298
-
299
- Scenario: Happy Path
300
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
301
- When I successfully run `fullstop file:///tmp/dotfiles.git`
302
- Then the dotfiles should be checked out in the directory "~/dotfiles"
303
- And the files in "~/dotfiles" should be symlinked in my home directory
304
- No such file or directory - .vimrc (Errno::ENOENT)
305
- ./features/step_definitions/fullstop_steps.rb:40:in `lstat'
306
- ./features/step_definitions/fullstop_steps.rb:40:in `block (3 levels) in <top (required)>'
307
- ./features/step_definitions/fullstop_steps.rb:39:in `each'
308
- ./features/step_definitions/fullstop_steps.rb:39:in `block (2 levels) in <top (required)>'
309
- ./features/step_definitions/fullstop_steps.rb:38:in `chdir'
310
- ./features/step_definitions/fullstop_steps.rb:38:in `/^the files in "([^"]*)" should be symlinked in my home directory$/'
311
- features/fullstop.feature:20:in `And the files in "~/dotfiles" should be symlinked in my home directory'
312
-
313
- Failing Scenarios:
314
- cucumber features/fullstop.feature:16
315
-
316
- 2 scenarios (1 failed, 1 passed)
317
- 11 steps (1 failed, 10 passed)
318
- 0m0.313s
319
- rake aborted!
320
- Cucumber failed
321
-
322
- Tasks: TOP => features
323
- (See full trace by running task with --trace)
324
- ```
325
-
326
- We're now failing at the next step:
327
-
328
- ```cucumber
329
- And the files in "~/dotfiles" should be symlinked in my home directory
330
- ```
331
-
332
- The error, "No such file or directory - .vimrc", is being raised from `File.lstat` (as opposed to an explicit test failure). 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 symlink them to the user's home directory. The tools to do this are already available to use via the built-in Ruby library `FileUtils`. We'll require it and implement the symlinking logic:
333
-
334
- ```ruby
335
- #!/usr/bin/env ruby
336
-
337
- require 'optparse'
338
- require 'methadone'
339
- require 'fullstop'
340
- # vvv
341
- require 'fileutils'
342
- # ^^^
343
-
344
- class App
345
- include Methadone::Main
346
- include Methadone::CLILogging
347
- include Methadone::SH
348
-
349
- main do |repo_url|
350
-
351
- Dir.chdir ENV['HOME'] do
352
- sh "git clone #{repo_url}"
353
- basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
354
- # vvv
355
- Dir.entries(basedir).each do |file|
356
- next if file == '.' || file == '..' || file == '.git'
357
- FileUtils.ln_s file,'.'
358
- end
359
- # ^^^
360
- end
361
- end
362
-
363
- version Fullstop::VERSION
364
-
365
- description 'Manages dotfiles from a git repo'
366
-
367
- arg :repo_url, "URL to the git repository containing your dotfiles"
368
-
369
- use_log_level_option
370
-
371
- go!
372
- end
373
- ```
374
-
375
- Now, let's run our scenario again:
376
-
377
- ```sh
378
- $ rake features
379
- Feature: Checkout dotfiles
380
- In order to get my dotfiles onto a new computer
381
- I want a one-command way to keep them up to date
382
- So I don't have to do it myself
383
-
384
- Scenario: Basic UI
385
- When I get help for "fullstop"
386
- Then the exit status should be 0
387
- And the banner should be present
388
- And there should be a one line summary of what the app does
389
- And the banner should include the version
390
- And the banner should document that this app takes options
391
- And the banner should document that this app's arguments are:
392
- | repo_url | which is required |
393
-
394
- Scenario: Happy Path
395
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
396
- When I successfully run `fullstop file:///tmp/dotfiles.git`
397
- Then the dotfiles should be checked out in the directory "~/dotfiles"
398
- And the files in "~/dotfiles" should be symlinked in my home directory
399
-
400
- 2 scenarios (2 passed)
401
- 11 steps (11 passed)
402
- 0m0.396s
403
- ```
404
-
405
- Everything passed! Our app now works for the "happy path". As long as the user starts from a clean home directory, `fullstop` 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.