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.
- data/bin/methadone +2 -2
- data/lib/methadone/cucumber.rb +6 -3
- data/lib/methadone/error.rb +3 -2
- data/lib/methadone/exit_now.rb +25 -0
- data/lib/methadone/main.rb +21 -10
- data/lib/methadone/sh.rb +19 -4
- data/lib/methadone/version.rb +1 -1
- data/lib/methadone.rb +1 -0
- data/templates/full/Rakefile.erb +1 -1
- data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -1
- data/test/test_exit_now.rb +37 -0
- data/test/test_main.rb +11 -5
- data/test/test_sh.rb +17 -0
- data/tutorial/.vimrc +6 -0
- data/tutorial/1_intro.md +52 -0
- data/tutorial/2_bootstrap.md +176 -0
- data/tutorial/3_ui.md +349 -0
- data/tutorial/4_happy_path.md +437 -0
- data/tutorial/5_more_features.md +702 -0
- data/tutorial/6_refactor.md +220 -0
- data/tutorial/7_logging_debugging.md +304 -0
- data/tutorial/8_conclusion.md +11 -0
- data/tutorial/code/.rvmrc +1 -0
- data/tutorial/code/fullstop/.gitignore +5 -0
- data/tutorial/code/fullstop/Gemfile +4 -0
- data/tutorial/code/fullstop/LICENSE.txt +202 -0
- data/tutorial/code/fullstop/README.rdoc +23 -0
- data/tutorial/code/fullstop/Rakefile +31 -0
- data/tutorial/code/fullstop/bin/fullstop +43 -0
- data/tutorial/code/fullstop/features/fullstop.feature +40 -0
- data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +64 -0
- data/tutorial/code/fullstop/features/support/env.rb +22 -0
- data/tutorial/code/fullstop/fullstop.gemspec +28 -0
- data/tutorial/code/fullstop/lib/fullstop/repo.rb +38 -0
- data/tutorial/code/fullstop/lib/fullstop/version.rb +3 -0
- data/tutorial/code/fullstop/lib/fullstop.rb +2 -0
- data/tutorial/code/fullstop/test/tc_something.rb +7 -0
- data/tutorial/en.utf-8.add +18 -0
- data/tutorial/toc.md +27 -0
- metadata +49 -20
data/bin/methadone
CHANGED
@@ -48,7 +48,7 @@ main do |app_name|
|
|
48
48
|
" #{gem_variable}.add_development_dependency('rdoc')",
|
49
49
|
" #{gem_variable}.add_development_dependency('aruba')",
|
50
50
|
" #{gem_variable}.add_development_dependency('rake','~> 0.9.2')",
|
51
|
-
" #{gem_variable}.add_dependency('methadone')",
|
51
|
+
" #{gem_variable}.add_dependency('methadone', '~>1.0.0.rc3')",
|
52
52
|
], :before => /^end\s*$/
|
53
53
|
end
|
54
54
|
|
@@ -64,7 +64,7 @@ on("-l LICENSE","--license",licenses,"Specify the license for your project (#{li
|
|
64
64
|
|
65
65
|
use_log_level_option
|
66
66
|
|
67
|
-
arg :app_name, :required
|
67
|
+
arg :app_name, :required, "Name of your app, which is used for the gem name and executable name"
|
68
68
|
|
69
69
|
version Methadone::VERSION
|
70
70
|
|
data/lib/methadone/cucumber.rb
CHANGED
@@ -34,7 +34,9 @@ module Methadone
|
|
34
34
|
#
|
35
35
|
# * Checks that the app's usage banner documents that its arguments are <tt>args</tt>
|
36
36
|
#
|
37
|
-
# Then the banner should document that this app's arguments are
|
37
|
+
# Then the banner should document that this app's arguments are
|
38
|
+
# |foo|which is optional|
|
39
|
+
# |bar|which is required|
|
38
40
|
#
|
39
41
|
# * Do the opposite; check that your app doesn't take any arguments
|
40
42
|
#
|
@@ -42,7 +44,7 @@ module Methadone
|
|
42
44
|
#
|
43
45
|
# * Check for a usage description which occurs after the banner and a blank line
|
44
46
|
#
|
45
|
-
# Then there should be a one
|
47
|
+
# Then there should be a one line summary of what the app does
|
46
48
|
#
|
47
49
|
module Cucumber
|
48
50
|
end
|
@@ -59,7 +61,7 @@ Then /^the following options should be documented:$/ do |options|
|
|
59
61
|
end
|
60
62
|
|
61
63
|
Then /^the option "([^"]*)" should be documented$/ do |option|
|
62
|
-
step %(the output should match
|
64
|
+
step %(the output should match /\\s*#{option}[\\s\\W]+\\w\\w\\w+/)
|
63
65
|
end
|
64
66
|
|
65
67
|
Then /^the banner should be present$/ do
|
@@ -75,6 +77,7 @@ Then /^the banner should document that this app's arguments are:$/ do |table|
|
|
75
77
|
expected_arguments = table.raw.map { |row|
|
76
78
|
option = row[0]
|
77
79
|
option = "[#{option}]" if row[1] == 'optional' || row[1] == 'which is optional'
|
80
|
+
option
|
78
81
|
}.join(' ')
|
79
82
|
step %(the output should contain "#{expected_arguments}")
|
80
83
|
end
|
data/lib/methadone/error.rb
CHANGED
@@ -16,8 +16,9 @@ module Methadone
|
|
16
16
|
# The command that caused the failure
|
17
17
|
attr_reader :command
|
18
18
|
|
19
|
-
def initialize(exit_code,command)
|
20
|
-
|
19
|
+
def initialize(exit_code,command,custom_error_message = nil)
|
20
|
+
error_message = String(custom_error_message).empty? ? "Command '#{command}' exited #{exit_code}" : custom_error_message
|
21
|
+
super(exit_code,error_message)
|
21
22
|
@command = command
|
22
23
|
end
|
23
24
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Methadone
|
2
|
+
module ExitNow
|
3
|
+
def self.included(k)
|
4
|
+
k.extend(self)
|
5
|
+
end
|
6
|
+
# Call this to exit the program immediately
|
7
|
+
# with the given error code and message.
|
8
|
+
#
|
9
|
+
# +exit_code+:: exit status you'd like to exit with
|
10
|
+
# +message+:: message to display to the user explaining the problem
|
11
|
+
#
|
12
|
+
# Also can be used without an exit code like so:
|
13
|
+
#
|
14
|
+
# exit_now!("Oh noes!")
|
15
|
+
#
|
16
|
+
# In this case, it's equivalent to <code>exit_now!(1,"Oh noes!")</code>.
|
17
|
+
def exit_now!(exit_code,message=nil)
|
18
|
+
if exit_code.kind_of?(String) && message.nil?
|
19
|
+
raise Methadone::Error.new(1,exit_code)
|
20
|
+
else
|
21
|
+
raise Methadone::Error.new(exit_code,message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/methadone/main.rb
CHANGED
@@ -61,6 +61,7 @@ module Methadone
|
|
61
61
|
# *not* do this.
|
62
62
|
#
|
63
63
|
module Main
|
64
|
+
include Methadone::ExitNow
|
64
65
|
def self.included(k)
|
65
66
|
k.extend(self)
|
66
67
|
end
|
@@ -118,7 +119,6 @@ module Methadone
|
|
118
119
|
@env_var = env_var
|
119
120
|
opts.separator ''
|
120
121
|
opts.separator "Default values can be placed in the #{env_var} environment variable"
|
121
|
-
opts.separator ''
|
122
122
|
end
|
123
123
|
|
124
124
|
# Start your command-line app, exiting appropriately when
|
@@ -136,6 +136,7 @@ module Methadone
|
|
136
136
|
#
|
137
137
|
def go!
|
138
138
|
normalize_defaults
|
139
|
+
opts.post_setup
|
139
140
|
if @env_var
|
140
141
|
String(ENV[@env_var]).split(/\s+/).each do |arg|
|
141
142
|
::ARGV.unshift(arg)
|
@@ -156,15 +157,6 @@ module Methadone
|
|
156
157
|
exit 64 # Linux standard for bad command line
|
157
158
|
end
|
158
159
|
|
159
|
-
# Call this to exit the program immediately
|
160
|
-
# with the given error code and message.
|
161
|
-
#
|
162
|
-
# +exit_code+:: exit status you'd like to exit with
|
163
|
-
# +message+:: message to display to the user explaining the problem
|
164
|
-
def exit_now!(exit_code,message=nil)
|
165
|
-
raise Methadone::Error.new(exit_code,message)
|
166
|
-
end
|
167
|
-
|
168
160
|
# Returns an OptionParser that you can use
|
169
161
|
# to declare your command-line interface. Generally, you
|
170
162
|
# won't use this and will use #on directly, but this allows
|
@@ -212,6 +204,7 @@ module Methadone
|
|
212
204
|
# <tt>:one</tt>:: only one of this arg should be supplied (default)
|
213
205
|
# <tt>:many</tt>:: many of this arg may be supplied, but at least one is required
|
214
206
|
# <tt>:any</tt>:: any number, include zero, may be supplied
|
207
|
+
# A string:: if present, this will be documentation for the argument and appear in the help
|
215
208
|
def arg(arg_name,*options)
|
216
209
|
opts.arg(arg_name,*options)
|
217
210
|
end
|
@@ -311,6 +304,7 @@ module Methadone
|
|
311
304
|
@accept_options = false
|
312
305
|
@args = []
|
313
306
|
@arg_options = {}
|
307
|
+
@arg_documentation = {}
|
314
308
|
@description = nil
|
315
309
|
@version = nil
|
316
310
|
set_banner
|
@@ -363,6 +357,9 @@ module Methadone
|
|
363
357
|
options << :one unless options.include?(:any) || options.include?(:many)
|
364
358
|
@args << arg_name
|
365
359
|
@arg_options[arg_name] = options
|
360
|
+
options.select { |_| _.kind_of? ::String }.each do |doc|
|
361
|
+
@arg_documentation[arg_name] = doc + (options.include?(:optional) ? " (optional)" : "")
|
362
|
+
end
|
366
363
|
set_banner
|
367
364
|
end
|
368
365
|
|
@@ -388,6 +385,20 @@ module Methadone
|
|
388
385
|
set_banner
|
389
386
|
end
|
390
387
|
|
388
|
+
# We need some documentation to appear at the end, after all OptionParser setup
|
389
|
+
# has occured, but before we actually start. This method serves that purpose
|
390
|
+
def post_setup
|
391
|
+
unless @arg_documentation.empty?
|
392
|
+
@option_parser.separator ''
|
393
|
+
@option_parser.separator "Arguments:"
|
394
|
+
@option_parser.separator ''
|
395
|
+
@args.each do |arg|
|
396
|
+
@option_parser.separator " #{arg}"
|
397
|
+
@option_parser.separator " #{@arg_documentation[arg]}"
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
391
402
|
private
|
392
403
|
|
393
404
|
def add_default_value_to_docstring(*args)
|
data/lib/methadone/sh.rb
CHANGED
@@ -57,6 +57,9 @@ module Methadone
|
|
57
57
|
# This is to make it easy for you to shell-out to external commands and have your app be robust and
|
58
58
|
# easy to maintain.
|
59
59
|
module SH
|
60
|
+
def self.included(k)
|
61
|
+
k.extend(self)
|
62
|
+
end
|
60
63
|
# Run a shell command, capturing and logging its output.
|
61
64
|
# If the command completed successfully, it's output is logged at DEBUG.
|
62
65
|
# If not, its output as logged at INFO. In either case, its
|
@@ -87,10 +90,10 @@ module Methadone
|
|
87
90
|
sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
|
88
91
|
|
89
92
|
if status.exitstatus != 0
|
90
|
-
sh_logger.info("Output of '#{command}': #{stdout}")
|
93
|
+
sh_logger.info("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
|
91
94
|
sh_logger.warn("Error running '#{command}'")
|
92
95
|
else
|
93
|
-
sh_logger.debug("Output of '#{command}': #{stdout}")
|
96
|
+
sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
|
94
97
|
call_block(block,stdout,stderr) unless block.nil?
|
95
98
|
end
|
96
99
|
|
@@ -103,10 +106,22 @@ module Methadone
|
|
103
106
|
# Run a command, throwing an exception if the command exited nonzero.
|
104
107
|
# Otherwise, behaves exactly like #sh.
|
105
108
|
#
|
109
|
+
# options - options hash, responding to:
|
110
|
+
# <tt>:on_fail</tt>:: a custom error message. This allows you to have your
|
111
|
+
# app exit on shell command failures, but customize the error
|
112
|
+
# message that they see.
|
113
|
+
#
|
106
114
|
# Raises Methadone::FailedCommandError if the command exited nonzero.
|
107
|
-
|
115
|
+
#
|
116
|
+
# Examples:
|
117
|
+
#
|
118
|
+
# sh!("rsync foo bar")
|
119
|
+
# # => if command fails, app exits and user sees: "error: Command 'rsync foo bar' exited 12"
|
120
|
+
# sh!("rsync foo bar", :on_fail => "Couldn't rsync, check log for details")
|
121
|
+
# # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
|
122
|
+
def sh!(command,options={},&block)
|
108
123
|
sh(command,&block).tap do |exitstatus|
|
109
|
-
raise Methadone::FailedCommandError.new(exitstatus,command) if exitstatus != 0
|
124
|
+
raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail]) if exitstatus != 0
|
110
125
|
end
|
111
126
|
end
|
112
127
|
|
data/lib/methadone/version.rb
CHANGED
data/lib/methadone.rb
CHANGED
data/templates/full/Rakefile.erb
CHANGED
@@ -17,7 +17,7 @@ end
|
|
17
17
|
CUKE_RESULTS = 'results.html'
|
18
18
|
CLEAN << CUKE_RESULTS
|
19
19
|
Cucumber::Rake::Task.new(:features) do |t|
|
20
|
-
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format
|
20
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x"
|
21
21
|
t.fork = false
|
22
22
|
end
|
23
23
|
|
@@ -1 +1 @@
|
|
1
|
-
# Put your step
|
1
|
+
# Put your step definitions here
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'base_test'
|
2
|
+
require 'methadone'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class TestExitNow < BaseTest
|
6
|
+
include Methadone
|
7
|
+
include Methadone::ExitNow
|
8
|
+
|
9
|
+
test_that "exit_now raises the proper error" do
|
10
|
+
Given {
|
11
|
+
@exit_code = any_int :min => 1
|
12
|
+
@message = any_string
|
13
|
+
}
|
14
|
+
When {
|
15
|
+
@code = lambda { exit_now!(@exit_code,@message) }
|
16
|
+
}
|
17
|
+
Then {
|
18
|
+
exception = assert_raises(Methadone::Error,&@code)
|
19
|
+
exception.exit_code.should == @exit_code
|
20
|
+
exception.message.should == @message
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
test_that "exit_now without an exit code uses 1 as the exti code" do
|
25
|
+
Given {
|
26
|
+
@message = any_string
|
27
|
+
}
|
28
|
+
When {
|
29
|
+
@code = lambda { exit_now!(@message) }
|
30
|
+
}
|
31
|
+
Then {
|
32
|
+
exception = assert_raises(Methadone::Error,&@code)
|
33
|
+
exception.exit_code.should == 1
|
34
|
+
exception.message.should == @message
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
data/test/test_main.rb
CHANGED
@@ -334,17 +334,23 @@ class TestMain < BaseTest
|
|
334
334
|
|
335
335
|
end
|
336
336
|
|
337
|
-
test_that "I can specify which arguments my app takes and if they are required" do
|
337
|
+
test_that "I can specify which arguments my app takes and if they are required as well as document them" do
|
338
338
|
Given {
|
339
339
|
main {}
|
340
|
+
@db_name_desc = any_string
|
341
|
+
@user_desc = any_string
|
342
|
+
@password_desc = any_string
|
340
343
|
|
341
|
-
arg :db_name
|
342
|
-
arg :user, :required
|
343
|
-
arg :password, :optional
|
344
|
+
arg :db_name, @db_name_desc
|
345
|
+
arg :user, :required, @user_desc
|
346
|
+
arg :password, :optional, @password_desc
|
344
347
|
}
|
345
|
-
|
348
|
+
When run_go_safely
|
346
349
|
Then {
|
347
350
|
opts.banner.should match /db_name user \[password\]$/
|
351
|
+
opts.to_s.should match /#{@db_name_desc}/
|
352
|
+
opts.to_s.should match /#{@user_desc}/
|
353
|
+
opts.to_s.should match /#{@password_desc}/
|
348
354
|
}
|
349
355
|
end
|
350
356
|
|
data/test/test_sh.rb
CHANGED
@@ -170,6 +170,23 @@ class TestSH < Clean::Test::TestCase
|
|
170
170
|
}
|
171
171
|
end
|
172
172
|
|
173
|
+
test_that "sh! runs a command that will fail and includes an error message that appears in the exception" do
|
174
|
+
Given {
|
175
|
+
use_capturing_logger
|
176
|
+
@command = test_command("foo")
|
177
|
+
@custom_error_message = any_sentence
|
178
|
+
}
|
179
|
+
When {
|
180
|
+
@code = lambda { sh! @command, :on_fail => @custom_error_message }
|
181
|
+
}
|
182
|
+
Then {
|
183
|
+
exception = assert_raises(Methadone::FailedCommandError,&@code)
|
184
|
+
exception.command.should == @command
|
185
|
+
exception.message.should == @custom_error_message
|
186
|
+
assert_logger_output_for_failure(@logger,@command,test_command_stdout,test_command_stderr)
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
173
190
|
class MyTestApp
|
174
191
|
include Methadone::SH
|
175
192
|
def initialize(logger)
|
data/tutorial/.vimrc
ADDED
data/tutorial/1_intro.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Awesome Ruby Command Line Apps with Methadone
|
2
|
+
|
3
|
+
Kick the bash habit, and make all your command-line apps with Ruby.
|
4
|
+
|
5
|
+
In [Build Awesome Command-Line Applications in Ruby][clibook], I lay out how to make an awesome command-line application using
|
6
|
+
Ruby. The book focuses on tools like `OptionParser` to create the app. As I wrote and researched, it became clear that there
|
7
|
+
was a gap between `OptionParser`, which is very powerful, yet verbose, and other command line tools like [trollop][trollop],
|
8
|
+
[main][main], and [thor][thor], which have simple APIs, but aren't very powerful.
|
9
|
+
|
10
|
+
[clibook]: http://www.awesomecommandlineapps.com
|
11
|
+
[main]: http://github.com/ahoward/main
|
12
|
+
[trollop]: http://trollop.rubyforge.org
|
13
|
+
[thor]: http://www.github.com/wycats/thor
|
14
|
+
|
15
|
+
I created Methadone to bridge that gap. Methadone provides all the power of `OptionParser`, but has a simple, clean API.
|
16
|
+
Methadone also includes additional tools and classes to make your command-line apps even better.
|
17
|
+
|
18
|
+
This tutorial will show you how to make a simple command-line app using Methadone that will be easy-to-use, easy-to-maintain, and
|
19
|
+
fully tested.
|
20
|
+
|
21
|
+
## What you'll need
|
22
|
+
|
23
|
+
You'll need an installation of Ruby and the ability to install Ruby gems. I would recommend that you use rvm and a gemset to
|
24
|
+
work through these, but they aren't required. There's a decent [walkthrough][setup] on my book's website of setting this up the
|
25
|
+
way I work. Although Methadone works on most versions of Ruby, I would recommend you use Ruby
|
26
|
+
1.9.3, if you can. If not, try to use an MRI Ruby as those versions (1.8.7, REE, 1.9.2, or 1.9.3) have the highest compatibility
|
27
|
+
with other gems.
|
28
|
+
|
29
|
+
[setup]: http://www.awesomecommandlineapps.com/setup.html
|
30
|
+
|
31
|
+
## How this is organized
|
32
|
+
|
33
|
+
This is a tutorial for making a simple command-line app. Unlike some tutorials and books, we will be working through this using
|
34
|
+
a "test-first" approach. One thing that Methadone tries to enable is using [TDD][tdd] for creating and writing your command-line
|
35
|
+
app. As such, we'll write tests as much as possible to drive our work.
|
36
|
+
|
37
|
+
[tdd]: http://en.wikipedia.org/wiki/Test-driven_development
|
38
|
+
|
39
|
+
## The tutorial app
|
40
|
+
|
41
|
+
The app we'll build is going to manage "dot files". These are the files that live in your home directory and configure your
|
42
|
+
shell, editor, and various other programs. For example, `~/.bashrc` is the file to configure `bash`. Many developers keep these
|
43
|
+
files on [Github][github] so that they can maintain the same files across multiple computers.
|
44
|
+
|
45
|
+
[github]: http://www.github.com
|
46
|
+
|
47
|
+
To set this up on a new computer, you have to checkout the repo, and symlink all the files to your home directory. To update the
|
48
|
+
files you have to update the repo and then check if any new files were added. This is the sort of tedious manual process that is
|
49
|
+
ripe for automation via a command-line app.
|
50
|
+
|
51
|
+
We'll develop a simplified version to demonstrate how to use Methadone.
|
52
|
+
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# Bootstrapping our app
|
2
|
+
|
3
|
+
One thing that's great about writing a webapp with Ruby on Rails is that, with one command, you have a skeleton app, including
|
4
|
+
a fully functional test framework set up. You can start writing tests immediately. There's no common equivalent for a
|
5
|
+
command-line app, which is what Methadone aims to provide.
|
6
|
+
|
7
|
+
Methadone will also bootstrap other aspects of your app, such a `Rakefile`, a gemspec, a shell of an executable, a license, and a
|
8
|
+
README. First, let's install Methadone via RubyGems (note that if your aren't using rvm, you may need to use `sudo` to install
|
9
|
+
gems):
|
10
|
+
|
11
|
+
```sh
|
12
|
+
$ gem install methadone
|
13
|
+
Fetching: methadone-1.0.0.gem (100%)
|
14
|
+
Successfully installed methadone-1.0.0
|
15
|
+
1 gem installed
|
16
|
+
Installing ri documentation for methadone-1.0.0...
|
17
|
+
Installing RDoc documentation for methadone-1.0.0...
|
18
|
+
```
|
19
|
+
|
20
|
+
Methadone comes bundled with a command-linen app that will do the bootstrapping:
|
21
|
+
|
22
|
+
```sh
|
23
|
+
$ methadone --help
|
24
|
+
Usage: methadone [options] app_name
|
25
|
+
|
26
|
+
Kick the bash habit by bootstrapping your Ruby command-line apps
|
27
|
+
|
28
|
+
v1.0.0
|
29
|
+
|
30
|
+
Options:
|
31
|
+
--force Overwrite files if they exist
|
32
|
+
--[no-]readme [Do not ]produce a README file
|
33
|
+
-l, --license LICENSE Specify the license for your project (mit|apache|custom|NONE)
|
34
|
+
--log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
35
|
+
(Default: info)
|
36
|
+
--version Show help/version info
|
37
|
+
|
38
|
+
Default values can be placed in the METHODONE_OPTS environment variable
|
39
|
+
```
|
40
|
+
|
41
|
+
The app that we'll be building in this tutorial is be called `fullstop`, which is derived from the [British name][fullstop] for a period, which is the character used as a prefix to our dotfiles, and, is the reason they are called "dot" files in the first place. Based on the command-line syntax for `methadone`, we can create our app right now with one simple command. We'll use the Apache license as well as a README.
|
42
|
+
|
43
|
+
[fullstop]: http://en.wikipedia.org/wiki/Full_stop
|
44
|
+
|
45
|
+
```sh
|
46
|
+
$ methadone --readme --license apache fullstop
|
47
|
+
$ ls fullstop
|
48
|
+
Gemfile README.rdoc bin/
|
49
|
+
fullstop.gemspec test/ LICENSE.txt
|
50
|
+
Rakefile features/ lib/
|
51
|
+
```
|
52
|
+
|
53
|
+
As you can see, we've got a generic gemified project. Before we can start developing, we'll need to install a few gems using Bundler first:
|
54
|
+
|
55
|
+
```sh
|
56
|
+
$ cd fullstop
|
57
|
+
$ bundle install
|
58
|
+
Fetching source index for http://rubygems.org/
|
59
|
+
Installing rake (0.9.2.2)
|
60
|
+
Installing ffi (1.0.11) with native extensions
|
61
|
+
Installing childprocess (0.3.1)
|
62
|
+
Installing builder (3.0.0)
|
63
|
+
Installing diff-lcs (1.1.3)
|
64
|
+
Installing json (1.6.5) with native extensions
|
65
|
+
Installing gherkin (2.7.6) with native extensions
|
66
|
+
Installing term-ansicolor (1.0.7)
|
67
|
+
Installing cucumber (1.1.4)
|
68
|
+
Installing rspec-core (2.8.0)
|
69
|
+
Installing rspec-expectations (2.8.0)
|
70
|
+
Installing rspec-mocks (2.8.0)
|
71
|
+
Installing rspec (2.8.0)
|
72
|
+
Installing aruba (0.4.11)
|
73
|
+
Using bundler (1.0.21)
|
74
|
+
Installing methadone (0.5.1)
|
75
|
+
Using fullstop (0.0.1) from source at /Users/davec/Projects/methadone/tutorial/code/fullstop
|
76
|
+
Installing rdoc (3.12)
|
77
|
+
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
|
78
|
+
```
|
79
|
+
|
80
|
+
Your versions might not match up, but this should be more or less what you see. The first thing you'll notice is that this seems
|
81
|
+
like a *lot* of gems! Most of them are brought in by our acceptance testing framework, [aruba][aruba], which is a library on top
|
82
|
+
of [cucumber][cucumber] tailor-made for testing command-line apps. Methadone also assumes you'll be unit
|
83
|
+
testing with `Test::Unit`, which is a fine default. In fact, both unit and acceptance tests are set up for you and available
|
84
|
+
via `rake` tasks. Let's see them in action.
|
85
|
+
|
86
|
+
[aruba]: http://www.github.com/cucumber/aruba
|
87
|
+
[cucumber]: http://cukes.info
|
88
|
+
|
89
|
+
```sh
|
90
|
+
$ rake
|
91
|
+
Run options:
|
92
|
+
|
93
|
+
# Running tests:
|
94
|
+
|
95
|
+
.
|
96
|
+
|
97
|
+
Finished tests in 0.000623s, 1605.1364 tests/s, 1605.1364 assertions/s.
|
98
|
+
|
99
|
+
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
100
|
+
......
|
101
|
+
|
102
|
+
1 scenario (1 passed)
|
103
|
+
6 steps (6 passed)
|
104
|
+
0m0.136s
|
105
|
+
```
|
106
|
+
|
107
|
+
As you can see, we ran one unit test and one cucumber scenario. These were provided by Methadone as placeholders for your tests.
|
108
|
+
Just like what Ruby on Rails does when you create an app, Methadone has reduced the friction between your need to write software
|
109
|
+
and your ability to do so.
|
110
|
+
|
111
|
+
Methadone also generated a very basic scaffold of the command-line app itself. It's in `bin` and is called `fullstop` (the
|
112
|
+
argument we gave to `methadone`). Let's run it now:
|
113
|
+
|
114
|
+
```sh
|
115
|
+
$ bin/fullstop
|
116
|
+
/Users/davec/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file -- fullstop (LoadError)
|
117
|
+
from /Users/davec/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
|
118
|
+
from bin/fullstop:5:in `<main>'
|
119
|
+
```
|
120
|
+
|
121
|
+
Oops! What happened?
|
122
|
+
|
123
|
+
Methadone is encouraging you to develop your app with best practices, and one such practice is to not have
|
124
|
+
your executables mess with the load path. In many Ruby command-line applications, you'll see code like this at the top of the
|
125
|
+
file:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
$: << File.join(File.dirname(__FILE__),'..','lib')
|
129
|
+
```
|
130
|
+
|
131
|
+
This puts the `lib` directory, that is relative to the `bin` directory (where our executable lives), into Ruby's load path. This will allow *us* to run the app easily, but for your users, it's not necessary if you distribute your app with RubyGems (which you should do) and it's generally not a good idea to modify the load path. In order to run the app directly during development, we'll need to use `bundle exec`, like so (note that we won't be running our app a lot in development, but scripting it using Aruba so we can test its behavior in an automated way):
|
132
|
+
|
133
|
+
```sh
|
134
|
+
$ bundle exec bin/fullstop --help
|
135
|
+
Usage: fullstop [options]
|
136
|
+
|
137
|
+
v0.0.1
|
138
|
+
|
139
|
+
Options:
|
140
|
+
--version Show help/version info
|
141
|
+
--log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
142
|
+
(Default: info)
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
Not too bad! We've got the makings of a reasonable help system, versioning support, a usage statement and a working executable.
|
147
|
+
Just remember to run the app with `bundle exec` while you're developing. Remember, your users won't have to worry about as long
|
148
|
+
as they installed it with RubyGems.
|
149
|
+
|
150
|
+
Before we move on, let's look at the cucumber scenario that Methadone generated for us. We're going to work "outside in" on our
|
151
|
+
app, so this will be a sneak peek at what we'll be doing next.
|
152
|
+
|
153
|
+
```sh
|
154
|
+
$ cat features/fullstop.feature
|
155
|
+
```
|
156
|
+
```cucumber
|
157
|
+
Feature: My bootstrapped app kinda works
|
158
|
+
In order to get going on coding my awesome app
|
159
|
+
I want to have aruba and cucumber setup
|
160
|
+
So I don't have to do it myself
|
161
|
+
|
162
|
+
Scenario: App just runs
|
163
|
+
When I get help for "fullstop"
|
164
|
+
Then the exit status should be 0
|
165
|
+
And the banner should be present
|
166
|
+
And the banner should document that this app takes options
|
167
|
+
And the following options should be documented:
|
168
|
+
|--version|
|
169
|
+
And the banner should document that this app takes no arguments
|
170
|
+
```
|
171
|
+
|
172
|
+
We probably won't keep this exactly scenario around, but it's a good demonstration of Aruba and Cucumber, and will help to get us
|
173
|
+
going. Since this scenario passes, that means that we already have the cucumber steps defined somewhere. As we'll see, the
|
174
|
+
combination of Aruba and Methadone results in a lot of pre-defined steps that make acceptance testing a snap.
|
175
|
+
|
176
|
+
In the next section, we'll expand this scenario to create the user interface we'll need to get our app going.
|