methadone-rehab 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +11 -0
  6. data/CHANGES.md +66 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +201 -0
  9. data/README.rdoc +179 -0
  10. data/Rakefile +98 -0
  11. data/TODO.md +3 -0
  12. data/bin/methadone +157 -0
  13. data/features/bootstrap.feature +169 -0
  14. data/features/license.feature +43 -0
  15. data/features/multilevel_commands.feature +125 -0
  16. data/features/readme.feature +26 -0
  17. data/features/rspec_support.feature +27 -0
  18. data/features/step_definitions/bootstrap_steps.rb +47 -0
  19. data/features/step_definitions/license_steps.rb +30 -0
  20. data/features/step_definitions/readme_steps.rb +26 -0
  21. data/features/step_definitions/version_steps.rb +4 -0
  22. data/features/support/env.rb +26 -0
  23. data/features/version.feature +17 -0
  24. data/lib/methadone.rb +15 -0
  25. data/lib/methadone/argv_parser.rb +50 -0
  26. data/lib/methadone/cli.rb +124 -0
  27. data/lib/methadone/cli_logger.rb +133 -0
  28. data/lib/methadone/cli_logging.rb +138 -0
  29. data/lib/methadone/cucumber.rb +174 -0
  30. data/lib/methadone/error.rb +32 -0
  31. data/lib/methadone/execution_strategy/base.rb +34 -0
  32. data/lib/methadone/execution_strategy/jvm.rb +37 -0
  33. data/lib/methadone/execution_strategy/mri.rb +16 -0
  34. data/lib/methadone/execution_strategy/open_3.rb +16 -0
  35. data/lib/methadone/execution_strategy/open_4.rb +22 -0
  36. data/lib/methadone/execution_strategy/rbx_open_4.rb +12 -0
  37. data/lib/methadone/exit_now.rb +40 -0
  38. data/lib/methadone/main.rb +1039 -0
  39. data/lib/methadone/process_status.rb +45 -0
  40. data/lib/methadone/sh.rb +223 -0
  41. data/lib/methadone/version.rb +3 -0
  42. data/methadone-rehab.gemspec +32 -0
  43. data/templates/full/.gitignore.erb +4 -0
  44. data/templates/full/README.rdoc.erb +25 -0
  45. data/templates/full/Rakefile.erb +74 -0
  46. data/templates/full/_license_head.txt.erb +2 -0
  47. data/templates/full/apache_LICENSE.txt.erb +203 -0
  48. data/templates/full/bin/executable.erb +47 -0
  49. data/templates/full/custom_LICENSE.txt.erb +0 -0
  50. data/templates/full/features/executable.feature.erb +13 -0
  51. data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -0
  52. data/templates/full/features/support/env.rb.erb +16 -0
  53. data/templates/full/gplv2_LICENSE.txt.erb +14 -0
  54. data/templates/full/gplv3_LICENSE.txt.erb +15 -0
  55. data/templates/full/mit_LICENSE.txt.erb +7 -0
  56. data/templates/multicommand/bin/executable.erb +52 -0
  57. data/templates/multicommand/lib/command.rb.erb +40 -0
  58. data/templates/multicommand/lib/commands.rb.erb +7 -0
  59. data/templates/rspec/spec/something_spec.rb.erb +5 -0
  60. data/templates/test_unit/test/tc_something.rb.erb +7 -0
  61. data/test/base_test.rb +20 -0
  62. data/test/command_for_tests.sh +7 -0
  63. data/test/execution_strategy/test_base.rb +24 -0
  64. data/test/execution_strategy/test_jvm.rb +77 -0
  65. data/test/execution_strategy/test_mri.rb +32 -0
  66. data/test/execution_strategy/test_open_3.rb +70 -0
  67. data/test/execution_strategy/test_open_4.rb +86 -0
  68. data/test/execution_strategy/test_rbx_open_4.rb +25 -0
  69. data/test/test_cli_logger.rb +219 -0
  70. data/test/test_cli_logging.rb +243 -0
  71. data/test/test_exit_now.rb +37 -0
  72. data/test/test_main.rb +1213 -0
  73. data/test/test_multi.rb +405 -0
  74. data/test/test_sh.rb +404 -0
  75. metadata +321 -0
@@ -0,0 +1,133 @@
1
+ require 'logger'
2
+
3
+ module Methadone
4
+ # A Logger instance that gives better control of messaging the user
5
+ # and logging app activity. At it's most basic, you would use <tt>info</tt>
6
+ # as a replacement for +puts+ and <tt>error</tt> as a replacement
7
+ # for <tt>STDERR.puts</tt>. Since this is a logger, however, you
8
+ # can also use #debug, #warn, and #fatal, and you can control
9
+ # the format and "logging level" as such.
10
+ #
11
+ # So, by default:
12
+ # * debug messages do not appear anywhere
13
+ # * info messages appear on the standard output
14
+ # * warn, error, and fatal messagse appear on the standard error
15
+ # * The default format of messages is simply the message, no logging cruft, however if your output
16
+ # is redirected to a file, a better timestamped logging format is used
17
+ #
18
+ # You can customize this in several ways:
19
+ #
20
+ # * You can override the devices used by passing different devices to the constructor
21
+ # * You can adjust the level of message that goes to the error logger via error_level=
22
+ # * You can adjust the format for messages to the error logger separately via error_formatter=
23
+ #
24
+ # === Example
25
+ #
26
+ # logger = CLILogger.new
27
+ # logger.debug("Starting up") # => only the standard output gets this
28
+ # logger.warn("careful!") # => only the standard error gets this
29
+ # logger.error("Something went wrong!") # => only the standard error gets this
30
+ #
31
+ # logger = CLILogger.new
32
+ # logger.error_level = Logger::ERROR
33
+ # logger.debug("Starting up") # => only the standard output gets this
34
+ # logger.warn("careful!") # => only the standard OUTPUT gets this
35
+ # logger.error("Something went wrong!") # => only the standard error gets this
36
+ #
37
+ # logger = CLILogger.new('logfile.txt')
38
+ # logger.debug("Starting up") # => logfile.txt gets this
39
+ # logger.error("Something went wrong!") # => BOTH logfile.txt AND the standard error get this
40
+ class CLILogger < Logger
41
+ BLANK_FORMAT = proc { |severity,datetime,progname,msg|
42
+ msg + "\n"
43
+ }
44
+
45
+ # Helper to proxy methods to the super class AND to the internal error logger
46
+ #
47
+ # +symbol+:: Symbol for name of the method to proxy
48
+ def self.proxy_method(symbol) #:nodoc:
49
+ old_name = "old_#{symbol}".to_sym
50
+ alias_method old_name,symbol
51
+ define_method symbol do |*args,&block|
52
+ send(old_name,*args,&block)
53
+ @stderr_logger.send(symbol,*args,&block)
54
+ end
55
+ end
56
+
57
+ proxy_method :'formatter='
58
+ proxy_method :'datetime_format='
59
+
60
+ def add(severity, message = nil, progname = nil, &block) #:nodoc:
61
+ if @split_logs
62
+ unless severity >= @stderr_logger.level
63
+ super(severity,message,progname,&block)
64
+ end
65
+ else
66
+ super(severity,message,progname,&block)
67
+ end
68
+ @stderr_logger.add(severity,message,progname,&block)
69
+ end
70
+
71
+ DEFAULT_ERROR_LEVEL = Logger::Severity::WARN
72
+
73
+ # A logger that logs error-type messages to a second device; useful
74
+ # for ensuring that error messages go to standard error. This should be
75
+ # pretty smart about doing the right thing. If both log devices are
76
+ # ttys, e.g. one is going to standard error and the other to the standard output,
77
+ # messages only appear once in the overall output stream. In other words,
78
+ # an ERROR logged will show up *only* in the standard error. If either
79
+ # log device is NOT a tty, then all messages go to +log_device+ and only
80
+ # errors go to +error_device+
81
+ #
82
+ # +log_device+:: device where all log messages should go, based on level
83
+ # +error_device+:: device where all error messages should go. By default, this is Logger::Severity::WARN
84
+ def initialize(log_device=$stdout,error_device=$stderr)
85
+ super(log_device)
86
+ @stderr_logger = Logger.new(error_device)
87
+
88
+ log_device_tty = tty?(log_device)
89
+ error_device_tty = tty?(error_device)
90
+
91
+ @split_logs = log_device_tty && error_device_tty
92
+
93
+ self.level = Logger::Severity::INFO
94
+ @stderr_logger.level = DEFAULT_ERROR_LEVEL
95
+
96
+ self.formatter = BLANK_FORMAT if log_device_tty
97
+ @stderr_logger.formatter = BLANK_FORMAT if error_device_tty
98
+ end
99
+
100
+ def level=(level)
101
+ super(level)
102
+ current_error_level = @stderr_logger.level
103
+ if (level > DEFAULT_ERROR_LEVEL) && @split_logs
104
+ @stderr_logger.level = level
105
+ end
106
+ end
107
+
108
+ # Set the threshold for what messages go to the error device. Note that calling
109
+ # #level= will *not* affect the error logger *unless* both devices are TTYs.
110
+ #
111
+ # +level+:: a constant from Logger::Severity for the level of messages that should go
112
+ # to the error logger
113
+ def error_level=(level)
114
+ @stderr_logger.level = level
115
+ end
116
+
117
+ # Overrides the formatter for the error logger. A future call to #formatter= will
118
+ # affect both, so the order of the calls matters.
119
+ #
120
+ # +formatter+:: Proc that handles the formatting, the same as for #formatter=
121
+ def error_formatter=(formatter)
122
+ @stderr_logger.formatter=formatter
123
+ end
124
+
125
+ private
126
+
127
+ def tty?(device_or_string)
128
+ return device_or_string.tty? if device_or_string.respond_to? :tty?
129
+ false
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,138 @@
1
+ module Methadone
2
+ # Provides easier access to a shared Methadone::CLILogger instance.
3
+ #
4
+ # Include this module into your class, and #logger provides access to a shared logger.
5
+ # This is handy if you want all of your clases to have access to the same logger, but
6
+ # don't want to (or aren't able to) pass it around to each class.
7
+ #
8
+ # This also provides methods for direct logging without going through the #logger
9
+ #
10
+ # === Example
11
+ #
12
+ # class MyClass
13
+ # include Methadone::CLILogger
14
+ #
15
+ # def doit
16
+ # debug("About to doit!")
17
+ # if results
18
+ # info("We did it!"
19
+ # else
20
+ # error("Something went wrong")
21
+ # end
22
+ # debug("Done doing it")
23
+ # end
24
+ # end
25
+ #
26
+ # Note that every class that mixes this in shares the *same logger instance*, so if you call #change_logger, this
27
+ # will change the logger for all classes that mix this in. This is likely what you want.
28
+ module CLILogging
29
+
30
+ def self.included(k)
31
+ k.extend(self)
32
+ end
33
+
34
+ # Access the shared logger. All classes that include this module
35
+ # will get the same logger via this method.
36
+ def logger
37
+ @@logger ||= CLILogger.new
38
+ end
39
+
40
+ # Change the global logger that includers will use. Useful if you
41
+ # don't want the default configured logger. Note that the +change_logger+
42
+ # version is preferred because Ruby will often parse <tt>logger = Logger.new</tt> as
43
+ # the declaration of, and assignment to, of a local variable. You'd need to
44
+ # do <tt>self.logger=Logger.new</tt> to be sure. This method
45
+ # is a bit easier.
46
+ #
47
+ # +new_logger+:: the new logger. May not be nil and should be a logger of some kind
48
+ def change_logger(new_logger)
49
+ raise ArgumentError,"Logger may not be nil" if new_logger.nil?
50
+ @@logger = new_logger
51
+ @@logger.level = @log_level if @log_level
52
+ end
53
+
54
+ alias logger= change_logger
55
+
56
+
57
+ # pass-through to <tt>logger.debug(progname,&block)</tt>
58
+ def debug(progname = nil, &block); logger.debug(progname,&block); end
59
+ # pass-through to <tt>logger.info(progname,&block)</tt>
60
+ def info(progname = nil, &block); logger.info(progname,&block); end
61
+ # pass-through to <tt>logger.warn(progname,&block)</tt>
62
+ def warn(progname = nil, &block); logger.warn(progname,&block); end
63
+ # pass-through to <tt>logger.error(progname,&block)</tt>
64
+ def error(progname = nil, &block); logger.error(progname,&block); end
65
+ # pass-through to <tt>logger.fatal(progname,&block)</tt>
66
+ def fatal(progname = nil, &block); logger.fatal(progname,&block); end
67
+
68
+ LOG_LEVELS = {
69
+ 'debug' => Logger::DEBUG,
70
+ 'info' => Logger::INFO,
71
+ 'warn' => Logger::WARN,
72
+ 'error' => Logger::ERROR,
73
+ 'fatal' => Logger::FATAL,
74
+ }
75
+
76
+ # Call this *if* you've included Methadone::Main to set up a <tt>--log-level</tt> option for your app
77
+ # that will allow the user to configure the logging level. You can pass an optional hash with
78
+ # <tt>:toggle_debug_on_signal => <SIGNAME></tt> to enable runtime toggling of the log level by sending the
79
+ # signal <tt><SIGNAME></tt> to your app
80
+ #
81
+ # +args+:: optional hash
82
+ #
83
+ # Example:
84
+ #
85
+ # main do
86
+ # # your app
87
+ # end
88
+ #
89
+ # use_log_level_option
90
+ #
91
+ # go!
92
+ #
93
+ # Example with runtime toggling:
94
+ #
95
+ #
96
+ # main do
97
+ # # your app
98
+ # end
99
+ #
100
+ # use_log_level_option :toggle_debug_on_signal => 'USR1'
101
+ #
102
+ # go!
103
+ def use_log_level_option(args = {})
104
+ on("--log-level LEVEL",LOG_LEVELS,'Set the logging level',
105
+ '(' + LOG_LEVELS.keys.join('|') + ')',
106
+ '(Default: info)', :global) do |level|
107
+ @log_level = level
108
+ @log_level_original = level
109
+ @log_level_toggled = false
110
+ logger.level = level
111
+
112
+ setup_toggle_trap(args[:toggle_debug_on_signal])
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ # Call this to toggle the log level between <tt>debug</tt> and its initial value
119
+ def toggle_log_level
120
+ @log_level_original = logger.level unless @log_level_toggled
121
+ logger.level = if @log_level_toggled
122
+ @log_level_original
123
+ else
124
+ LOG_LEVELS['debug']
125
+ end
126
+ @log_level_toggled = !@log_level_toggled
127
+ @log_level = logger.level
128
+ end
129
+
130
+ def setup_toggle_trap(signal)
131
+ if signal
132
+ Signal.trap(signal) do
133
+ toggle_log_level
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,174 @@
1
+ module Methadone
2
+ # By <tt>require</tt>'ing <tt>methadone/cucumber</tt> in your Cucumber setup (e.g. in <tt>env.rb</tt>), you
3
+ # gain access to the steps defined in this file. They provide you with the following:
4
+ #
5
+ # * Run <tt>command_to_run --help</tt> using aruba
6
+ #
7
+ # When I get help for "command_to_run"
8
+ #
9
+ # * Make sure that each option shows up in the help and has *some* sort of documentation. By default,
10
+ # the options won't be required to be negatable.
11
+ #
12
+ # Then the following options should be documented:
13
+ # |--force|
14
+ # |-x |
15
+ #
16
+ # Then the following options should be documented:
17
+ # |--force| which is negatable |
18
+ # |-x | which is not negatable |
19
+ #
20
+ # * Check an individual option for documentation:
21
+ #
22
+ # Then the option "--force" should be documented
23
+ # Then the option "--force" should be documented which is negatable
24
+ #
25
+ # * Checks that the help has a proper usage banner
26
+ #
27
+ # Then the banner should be present
28
+ #
29
+ # * Checks that the banner includes the version
30
+ #
31
+ # Then the banner should include the version
32
+ #
33
+ # * Checks that the usage banner indicates it takes options via <tt>[options]</tt>
34
+ #
35
+ # Then the banner should document that this app takes options
36
+ #
37
+ # * Do the opposite; check that you don't indicate options are accepted
38
+ #
39
+ # Then the banner should document that this app takes no options
40
+ #
41
+ # * Checks that the app's usage banner documents that its arguments are <tt>args</tt>
42
+ #
43
+ # Then the banner should document that this app's arguments are
44
+ # |foo|which is optional|
45
+ # |bar|which is required|
46
+ #
47
+ # * Do the opposite; check that your app doesn't take any arguments
48
+ #
49
+ # Then the banner should document that this app takes no arguments
50
+ #
51
+ # * Check for a usage description which occurs after the banner and a blank line
52
+ #
53
+ # Then there should be a one line summary of what the app does
54
+ #
55
+ module Cucumber
56
+ end
57
+ end
58
+
59
+ Given /^PENDING/ do
60
+ pending "test needs to be writen"
61
+ end
62
+
63
+ When /^I get help for "([^"]*)"$/ do |app_name|
64
+ @app_name = app_name
65
+ step %(I run `#{app_name} --help`)
66
+ end
67
+
68
+ When /^I get help for "([^"]*)" subcommand "([^"]*)"$/ do |app_name,subcommands|
69
+ @app_name = app_name
70
+ @subcommands = subcommands.split(/\s+/)
71
+ step %(I run `#{app_name} #{subcommands} --help`)
72
+ end
73
+
74
+ Then /^the following (argument|option)s should be documented:$/ do |type,table|
75
+ table.raw.each do |row|
76
+ step %(the #{type} "#{row[0]}" should be documented #{row[1]})
77
+ end
78
+ end
79
+
80
+ Then /^there should be (\d+) options listed$/ do |option_count|
81
+ match = all_output.match(/(?m)Options:\n((?:(?!\n\n).)*)(?:\n\n|\z)/)
82
+ real_option_count = match[1].chomp.split(/\n/).select {|l| l =~ /^ *-/}.length
83
+ real_option_count.should == option_count.to_i
84
+ end
85
+
86
+ Then /^the option "([^"]*)" should be documented(.*)$/ do |options,qualifiers|
87
+ options.split(',').map(&:strip).each do |option|
88
+ if qualifiers.strip == "which is negatable"
89
+ option = option.gsub(/^--/,"--[no-]")
90
+ end
91
+ step %(the output should match /\\s*#{Regexp.escape(option)}[\\s\\W]+\\w[\\s\\w][\\s\\w]+/)
92
+ end
93
+ end
94
+
95
+ Then /^the following (commands|global options) should be documented:$/ do |type, table|
96
+ table.raw.each do |row|
97
+ step %(the output should match /(?m)#{type.capitalize}:((?!\\n\\n).)*\\n +#{Regexp.escape(row[0])}/)
98
+ end
99
+ end
100
+
101
+ Then /^there should be (\d+) command listed$/ do |command_count|
102
+ match = all_output.match(/(?m)Commands:\n((?: [^\n]*\n)+)(\n|\z)/)
103
+ real_command_count = match[1].chomp.split(/\n/).length
104
+ real_command_count.should == command_count.to_i.should
105
+ end
106
+
107
+ Then /^the banner should be present$/ do
108
+ step %(the output should match /Usage: #{@app_name}/)
109
+ end
110
+
111
+ Then /^the banner should document that this app takes options$/ do
112
+ step %(the output should match /\[options\]/)
113
+ step %(the output should contain "Options")
114
+ end
115
+
116
+ Then /^the banner should document that this app takes global options$/ do
117
+ step %(the output should match /\[global options\]/)
118
+ step %(the output should contain "Global options")
119
+ end
120
+
121
+ Then /^the banner should document that this app takes commands$/ do
122
+ step %(the output should match /command \[command options and args...\]/)
123
+ step %(the output should contain "\\nCommands:")
124
+ end
125
+
126
+ Then /^the banner should document that this app's arguments are:$/ do |table|
127
+ expected_arguments = table.raw.map { |row|
128
+ option = row[0]
129
+ option = "#{option}..." if row[1] =~ /(?:which can take )?(many|any)( values)?/
130
+ option = "[#{option}]" if row[1] =~ /(which is optional|which can take any( values)?|optional|any)/
131
+ option
132
+ }.join(' ')
133
+ step %(the output should contain "#{expected_arguments}")
134
+ end
135
+
136
+ Then /^there should be (\d+) arguments? listed$/ do |arg_count|
137
+ match = all_output.match(/(?m)\nArguments:\n((?: [^\n]*\n)+)(?:\n|\z)/)
138
+ real_arg_count = match[1].chomp.split(/\n (?=[^ ])/).length
139
+ real_arg_count.should == arg_count.to_i
140
+ end
141
+
142
+ Then /^the argument "([^"]*)" should be documented(.*)$/ do |arg,qualifiers|
143
+ if qualifiers.strip == "which is optional"
144
+ arg += " (optional)"
145
+ end
146
+ step %(the output should match /(?m)Arguments:((?!\\n\\n).)*\\n #{Regexp.escape(arg)}/)
147
+ end
148
+
149
+ Then /^the banner should document that this app takes no options$/ do
150
+ step %(the output should not contain "[options]")
151
+ step %(the output should not contain "^Options:")
152
+ end
153
+
154
+ Then /^the banner should document that this app takes no arguments$/ do
155
+ step %(the output should match /Usage: #{@app_name}\\s*\(\\[options\\]\)?$/)
156
+ end
157
+
158
+ Then /^the banner should document that this app takes no commands$/ do
159
+ step %(the output should not match /command \[command options and args...\]/)
160
+ step %(the output should not contain "\\nCommands:")
161
+ end
162
+
163
+ Then /^the banner should include the version$/ do
164
+ step %(the output should match /v\\d+\\.\\d+\\.\\d+/)
165
+ end
166
+
167
+ Then /^there should be a one line summary of what the app does$/ do
168
+ output_lines = all_output.split(/\n/)
169
+ output_lines.size.should >= 4
170
+ # [0] is a blank line,
171
+ # [1] is our banner, which we've checked for
172
+ output_lines[2].should match(/^\s*$/)
173
+ output_lines[3].should match(/^\w+\s+\w+/)
174
+ end
@@ -0,0 +1,32 @@
1
+ module Methadone
2
+ # Standard exception you can throw to exit with a given
3
+ # status code. Generally, you should prefer Methadone::Main#exit_now! over using
4
+ # this directly, however you may wish to create a rich hierarchy of exceptions that extend from
5
+ # this in your app, so this is provided if you wish to do so.
6
+ class Error < StandardError
7
+ attr_reader :exit_code
8
+ # Create an Error with the given status code and message
9
+ def initialize(exit_code,message=nil)
10
+ super(message)
11
+ @exit_code = exit_code
12
+ end
13
+ end
14
+
15
+ # Thrown by certain methods when an externally-called command exits nonzero
16
+ class FailedCommandError < Error
17
+
18
+ # The command that caused the failure
19
+ attr_reader :command
20
+
21
+ # exit_code:: exit code of the command that caused this
22
+ # command:: the entire command-line that caused this
23
+ # custom_error_message:: an error message to show the user instead of the boilerplate one. Useful
24
+ # for allowing this exception to bubble up and exit the program, but to give
25
+ # the user something actionable.
26
+ def initialize(exit_code,command,custom_error_message = nil)
27
+ error_message = String(custom_error_message).empty? ? "Command '#{command}' exited #{exit_code}" : custom_error_message
28
+ super(exit_code,error_message)
29
+ @command = command
30
+ end
31
+ end
32
+ end