dev-utils 1.0

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.
@@ -0,0 +1,176 @@
1
+ # = dev-utils/debug/irb.rb
2
+ #
3
+ # DevUtils::Debug.breakpoint and some aliases are implemented here. Do not load this file
4
+ # directly.
5
+ #
6
+
7
+ require 'irb'
8
+ require 'extensions/binding'
9
+
10
+ module DevUtils::Debug
11
+
12
+ #
13
+ # Method:: *breakpoint*
14
+ # Aliases:: *break_point*, *goirb*
15
+ #
16
+ # === Description
17
+ #
18
+ # This will pop up an interactive ruby session from whereever it is called in a Ruby
19
+ # application. In IRB you can examine the environment of the break point, peeking
20
+ # and poking local variables, calling methods, viewing the stack (with +caller+), etc.
21
+ #
22
+ # This is like setting breakpoints in a debugger, except that you are running the program
23
+ # normally (debuggers tend to run programs more slowly). Debuggers are generally more
24
+ # flexible, but this is a good lightweight solution for many cases. You <em>can not</em>
25
+ # step through the code with this technique. But you can, of course, set multiple
26
+ # breakpoints. And you can make breakpoints conditional.
27
+ #
28
+ # You can force a breakpoint to return a certain value. This is typically only useful if
29
+ # the breakpoint is the last value in a method, as this will cause the method to return a
30
+ # different value than normal. This is demonstrated in the example below.
31
+ #
32
+ # You can also give names to break points which will be used in the message that is
33
+ # displayed upon execution of them. This helps to differentiate them at runtime if you
34
+ # have set several breakpoints in the code.
35
+ #
36
+ # === Parameters
37
+ #
38
+ # _name_:: A String to identify the breakpoint, giving a more informative message.
39
+ # _context_:: Any object; IRB will use this as its context. The default is the current
40
+ # scope's binding, which is nearly always what you will want.
41
+ # _block_:: Will be executed when the breakpoint returns normally. Bypassed if you
42
+ # <tt>throw :debug_return, </tt><em>value</em> from IRB. Unless you are
43
+ # _planning_ to use the <tt>debug_return</tt> feature for a given breakpoint,
44
+ # you don't need to worry about the block.
45
+ #
46
+ # === Typical Invocation
47
+ #
48
+ # breakpoint # Plain message.
49
+ # breakpoint "Person#name" # More informative message.
50
+ # breakpoint { normal_return_value }
51
+ #
52
+ # === Example
53
+ #
54
+ # Here's a sample of how breakpoints should be placed:
55
+ #
56
+ # require 'dev-utils/debug'
57
+ #
58
+ # class Person
59
+ # def initialize(name, age)
60
+ # @name, @age = name, age
61
+ # breakpoint "Person#initialize"
62
+ # end
63
+ #
64
+ # attr_reader :age
65
+ # def name
66
+ # breakpoint "Person#name" { @name }
67
+ # end
68
+ # end
69
+ #
70
+ # person = Person.new("Random Person", 23)
71
+ # puts "Name: #{person.name}"
72
+ #
73
+ # And here is a sample debug session:
74
+ #
75
+ # Executing break point "Person#initialize" at file.rb:4 in `initialize'
76
+ # irb(#<Person:0x292fbe8>):001:0> <b>local_variables</b>
77
+ # => ["name", "age", "_", "__"]
78
+ # irb(#<Person:0x292fbe8>):002:0> [name, age]
79
+ # => ["Random Person", 23]
80
+ # irb(#<Person:0x292fbe8>):003:0> [@name, @age]
81
+ # => ["Random Person", 23]
82
+ # irb(#<Person:0x292fbe8>):004:0> self
83
+ # => #<Person:0x292fbe8 @age=23, @name="Random Person">
84
+ # irb(#<Person:0x292fbe8>):005:0> @age += 1; self
85
+ # => #<Person:0x292fbe8 @age=24, @name="Random Person">
86
+ # irb(#<Person:0x292fbe8>):006:0> exit
87
+ # Executing break point "Person#name" at file.rb:9 in `name'
88
+ # irb(#<Person:0x292fbe8>):001:0> throw(:debug_return, "Overriden name")
89
+ # Name: Overriden name
90
+ #
91
+ # This example is explored more thoroughly at
92
+ # http://dev-utils.rubyforge.org/DebuggingAids.html.
93
+ #
94
+ # === Credits
95
+ #
96
+ # Joel VanderWerf and Florian Gross have contributed the code and documentation for this
97
+ # method.
98
+ #
99
+ def breakpoint(name = nil, context = nil, &block)
100
+ file, line, method = *caller.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
101
+ message = "Executing breakpoint #{name.inspect if name} at #{file}:#{line}"
102
+ message << " in '#{method}'" if method
103
+
104
+ body = lambda do |_context|
105
+ puts message
106
+ catch(:debug_return) do |value|
107
+ IRB.start_session(IRB::WorkSpace.new(_context))
108
+ block.call if block
109
+ end
110
+ end
111
+
112
+ # Run IRB with the given context, if it _is_ given.
113
+ return body.call(context) if context
114
+ # Otherwise, run IRB with the parent scope's binding, giving access to
115
+ # the local variables of the method that called method.
116
+ Binding.of_caller do |binding_context|
117
+ body.call(binding_context)
118
+ end
119
+ end
120
+
121
+ alias :break_point :breakpoint
122
+ alias :goirb :breakpoint
123
+
124
+ alias lv local_variables
125
+ alias iv instance_variables
126
+ end
127
+
128
+ # :stopdoc:
129
+
130
+ module ::IRB
131
+
132
+ class << self; remove_method :parse_opts; end
133
+ def IRB.parse_opts
134
+ # Don't touch ARGV, which belongs to the app which called this module.
135
+ end
136
+
137
+ def IRB.start_session(object)
138
+ unless $irb
139
+ IRB.setup nil
140
+ ## maybe set some opts here, as in parse_opts in irb/init.rb?
141
+ end
142
+
143
+ @CONF[:PROMPT_MODE] = :SIMPLE
144
+
145
+ workspace = WorkSpace.new(object)
146
+
147
+ if @CONF[:SCRIPT] ## normally, set by parse_opts
148
+ $irb = Irb.new(workspace, @CONF[:SCRIPT])
149
+ else
150
+ $irb = Irb.new(workspace)
151
+ end
152
+
153
+ @CONF[:IRB_RC].call($irb.context) if @CONF[:IRB_RC]
154
+ @CONF[:MAIN_CONTEXT] = $irb.context
155
+
156
+ trap 'INT' do
157
+ $irb.signal_handle
158
+ end
159
+
160
+ custom_configuration if defined?(IRB.custom_configuration)
161
+
162
+ catch :IRB_EXIT do
163
+ $irb.eval_input
164
+ end
165
+
166
+ puts
167
+
168
+ ## might want to reset your app's interrupt handler here
169
+ end
170
+ end
171
+
172
+ class Object
173
+ include IRB::ExtendCommandBundle # so that Marshal.dump works
174
+ end
175
+
176
+ # :startdoc:
@@ -0,0 +1,107 @@
1
+ # = dev-utils/debug/log.rb
2
+ #
3
+ # DevUtils::Debug.{debug,trace,logger=,logfile=} are implemented here. Do not load this file
4
+ # directly.
5
+ #
6
+
7
+ require 'logger'
8
+ require 'extensions/binding'
9
+ require 'extensions/object'
10
+
11
+ module DevUtils::Debug
12
+
13
+ # The target of logging messages from _debug_ and _trace_.
14
+ DEBUGLOG = Logger.new(File.new('debug.log', 'w'))
15
+ DEBUGLOG.datetime_format = " \010"
16
+ DEBUGLOG.progname = "\010\010\010"
17
+
18
+ #
19
+ # Write _message_ to the debugging log.
20
+ #
21
+ # The <em>debugging log</em> is a zero-conf logfile. Here is an example usage:
22
+ #
23
+ # $ cat example.rb
24
+ #
25
+ # require 'dev-utils/debug'
26
+ #
27
+ # debug "Setting variables x and y"
28
+ # x = 5; y = 17
29
+ # trace 'x + y'
30
+ # puts "Finished"
31
+ #
32
+ # $ ruby example.rb
33
+ # Finished
34
+ #
35
+ # $ cat debug.log
36
+ # D, [#244] DEBUG : Setting variables x and y
37
+ # D, [#244] DEBUG : x + y = 22
38
+ #
39
+ # Simply with <tt>require 'dev-utils/debug'</tt>, you have availed yourself of a handy
40
+ # debugging log file which you don't have to create.
41
+ #
42
+ # See also the _trace_ method.
43
+ #
44
+ def debug(message)
45
+ DEBUGLOG.debug message
46
+ end
47
+
48
+ #
49
+ # Prints a trace message to DEBUGLOG (at debug level). Useful for emitting
50
+ # the value of variables, etc. Use like this:
51
+ #
52
+ # x = y = 5
53
+ # trace 'x' # -> 'x = 5'
54
+ # trace 'x ** y' # -> 'x ** y = 3125'
55
+ #
56
+ # If you have a more complicated value, like an array of hashes, then you'll probably want
57
+ # to use an alternative output format. For instance:
58
+ #
59
+ # trace 'value', :yaml
60
+ #
61
+ # Valid output format values (the _style_ parameter) are:
62
+ #
63
+ # :p :inspect
64
+ # :pp (pretty-print, using 'pp' library)
65
+ # :s :to_s
66
+ # :y :yaml :to_yaml (using the 'yaml' library')
67
+ #
68
+ # The default is <tt>:p</tt>.
69
+ #
70
+ def trace(expr, style=:p)
71
+ unless expr.respond_to? :to_str
72
+ message = "trace: Can't evaluate the given value: #{caller.first}"
73
+ DEBUGLOG.warn message
74
+ else
75
+ Binding.of_caller do |b|
76
+ value = b.eval(expr.to_str)
77
+ formatter = TRACE_STYLES[style] || :inspect
78
+ case formatter
79
+ when :pp then require 'pp'
80
+ when :y, :yaml, :to_yaml then require 'yaml'
81
+ end
82
+ value_s = value.send(formatter)
83
+ message = "#{expr} = #{value_s}"
84
+ lines = message.split(/\n/)
85
+ indent = " "
86
+ DEBUGLOG.debug lines.shift # First line unindented.
87
+ lines.each do |line|
88
+ DEBUGLOG.debug(indent + line) # Other lines indented.
89
+ end
90
+ end
91
+ end
92
+ end
93
+ TRACE_STYLES = {} # :nodoc:
94
+ TRACE_STYLES.update( :pp => :pp_s, :s => :to_s, :p => :inspect, :y => :to_yaml,
95
+ :yaml => :to_yaml, :inspect => :inspect, :to_yaml => :to_yaml )
96
+
97
+ # Planned feature; not yet implemented.
98
+ def logger=(x)
99
+ warn 'logger= is not implemented; ignoring'
100
+ end
101
+
102
+ # Planned feature; not yet implemented.
103
+ def logfile=(x)
104
+ warn 'logfile= is not implemented; ignoring'
105
+ end
106
+
107
+ end # module DevUtils::Debug
@@ -0,0 +1,267 @@
1
+ #
2
+ # = dev-utils/test.rb
3
+ #
4
+ # _Planned_ functionality to support <b>unit testing</b>. Nothing here at the moment.
5
+ #
6
+
7
+ =begin
8
+
9
+ #
10
+ # = dev-utils/test.rb
11
+ #
12
+ # Testing utilities DevUtils::Test.{load_tests,load_data} and various project directory access
13
+ # methods are implemented here. See DevUtils::Test for documentation.
14
+ #
15
+
16
+ require 'test/unit'
17
+ require 'test/unit/ui/console/testrunner'
18
+ require 'optparse'
19
+ require 'ostruct'
20
+
21
+ module DevUtils
22
+ #
23
+ # ...
24
+ #
25
+ class Test
26
+
27
+ #
28
+ # Loads all unit test files matching the arguments given in the sense described below.
29
+ # A typical use for this is:
30
+ #
31
+ # DevUtils::Test.load_tests(__FILE__, *ARGV)
32
+ #
33
+ # That will load all 'tc_*.rb' files in or below the directory in which the calling
34
+ # program is located. If any arguments are provides, they are substrings used to
35
+ # restrict the tests that are run, giving the user an easy way to select a unit test.
36
+ #
37
+ # For example, if the arguments "str" and "num" are given, then for any test file to be
38
+ # loaded, its filename must contain either or both of those substrings. It's a hassle
39
+ # for the user to type the whole filename, so this makes test selection more practical.
40
+ #
41
+ # I'll experiment with accepting other arguments like "-q" and "-v" to mean "quiet" and
42
+ # "verbose" respectively. Verbose is default, meaning each test filename is relayed to
43
+ # STDERR to inform the user what's going on. Other arguments: "-h" for help (to explain
44
+ # this whole thing), and "-s" for separate running of each test, instead of running all
45
+ # tests in one big suite.
46
+ #
47
+ # The <b>ultimate purpose</b> of this is to implement a unit test suite runner in a
48
+ # reusable fashion, rather than coding one up for each project. Therefore, the file
49
+ # <tt>test/TEST.rb</tt>, for instance, would consist of:
50
+ #
51
+ # require 'rubygems'
52
+ # require 'dev-utils/test'
53
+ #
54
+ # DevUtils::Test.load_tests(__FILE__, ARGV)
55
+ #
56
+ # The user can then simply run:
57
+ #
58
+ # ruby test/TEST.rb -h # get help on options
59
+ # ruby test/TEST.rb -q # quiet operation
60
+ # ruby test/TEST.rb -s # separate suite per test file
61
+ # ruby test/TEST.rb str io # only tests that have "str" or "io" in the filename
62
+ #
63
+ # The convention of naming test files "tc_XYZ.rb" must be followed for this to work.
64
+ #
65
+ def self.load_tests(path, args)
66
+ if self.project_dir.nil?
67
+ # This is the usual case. We derive the project directory from the path. The fact
68
+ # that @project_dir is nil means that all defaults have been accepted. Therefore we
69
+ # must be in the +test+ directory.
70
+ path = File.expand_path(path)
71
+ unless FileTest.exist?(path)
72
+ raise RuntimeError, "Internal state problem: path '#{path}' doesn't exist"
73
+ end
74
+ test_dir = File.dirname(path)
75
+ project_dir = File.dirname(test_dir)
76
+ self.project_dir = project_dir
77
+ _assert_path self.lib_dir
78
+ _assert_path self.test_dir
79
+ # We don't assert the existence of the test data directory because it's not
80
+ # mandatory.
81
+ else
82
+ # In this case, the project directory has been specified prior to calling this
83
+ # method. We will simply use the values provided or derived.
84
+ end
85
+
86
+ # The main things to do in this method: modify the load path; parse the arguments
87
+ # given, decide which test files to load, and load them.
88
+
89
+ # 1. Handle the load path.
90
+ $:.unshift self.lib_dir
91
+
92
+ # 2. Parse the arguments given to this method.
93
+ opts = _parse_options(args)
94
+
95
+ Dir.chdir(self.test_dir) do
96
+ # 3. Decide which test files to load.
97
+ test_files = Dir['**/tc_*.rb']
98
+ unless opts.patterns.empty?
99
+ test_files = test_files.select { |fn| opts.patterns.any? { |str| fn.index(str) } }
100
+ end
101
+
102
+ # 4. Load them, using separate test suites if requested.
103
+ test_files.each do |test_file|
104
+ STDERR.puts "Loading test file: #{test_file}" if opts.mode == :verbose
105
+ require test_file
106
+ # TODO: take care of separate test suites, and respect --very-quiet.
107
+ end
108
+ if opts.separate
109
+ test_cases = []
110
+ ObjectSpace.each_object(Class) do |klass|
111
+ test_cases << klass if klass < ::Test::Unit::TestCase
112
+ end
113
+ test_cases.sort_by { |c| c.name }.each do |klass|
114
+ STDERR.puts "Running test class #{klass}" if opts.mode == :verbose
115
+ output_level =
116
+ case opts.mode
117
+ when :veryquiet then ::Test::Unit::UI::SILENT
118
+ when :quiet then ::Test::Unit::UI::PROGRESS_ONLY
119
+ when :verbose then ::Test::Unit::UI::NORMAL
120
+ end
121
+ runner = ::Test::Unit::UI::Console::TestRunner.new(klass.suite, output_level)
122
+ runner.start
123
+ end
124
+ else
125
+ # We let all test cases run in a single suite on exit.
126
+ end
127
+ end
128
+ end # def load_tests
129
+
130
+ #
131
+ # This method is aimed at allowing unit tests to access data resources easily. Those
132
+ # resources are files buried in a <i>test data</i> directory, so they do not clutter the
133
+ # unit tests themselves.
134
+ #
135
+ # You access a test data resource by its path relative to the test data directory. The
136
+ # unit test runs blissfully unaware of where that directory is. See ... for a
137
+ # demonstration. (TODO: write a high-level document that ties this all together.)
138
+ #
139
+ # Once the test data resource is located, the default is to load the file and return its
140
+ # contents as a string. You can get different behaviour by specifying the XXX
141
+ #
142
+ # The default is to return a String. The +flags+ provided affect this:
143
+ # * <tt>:string</tt> means return a String
144
+ # * <tt>:file</tt> means return a File
145
+ # * <tt>:pathname</tt> means return a Pathname
146
+ # * <tt>:copy</tt> means return a _copy_ of the file (<tt>:file</tt> and
147
+ # <tt>:pathname</tt> only)
148
+ # * <tt>:write</tt> means resource is intended to be _written_, therefore don't complain
149
+ # if it doesn't exist (<tt>:file</tt> and <tt>:pathname</tt> only)
150
+ #
151
+ # An error of some sort is raised if the file doesn't exist, unless <tt>:write</tt> is
152
+ # specified.
153
+ #
154
+ def self.load_data(resource_path, *flags)
155
+ if self.data_directory.nil?
156
+ raise RuntimeError,
157
+ "Test data directory hasn't been defined. Use #{self}.data_directory="
158
+ end
159
+ end
160
+
161
+ def self.set_project_dir_from(path, project_name)
162
+ path = File.expand_path(path)
163
+ _assert_path path
164
+ # The project dir is everything in the path up to and including the first instance of the
165
+ # project name.
166
+ if path =~ /\A(.*?#{project_name}).*\Z/
167
+ self.project_dir = $1
168
+ else
169
+ raise RuntimeError, "Project name '#{project_name}' not found in path '#{path}'"
170
+ end
171
+ end
172
+
173
+ def self.project_dir=(path)
174
+ path = File.expand_path(path)
175
+ _assert_path path
176
+ @project_dir = path
177
+ end
178
+
179
+ def self.project_dir
180
+ @project_dir
181
+ end
182
+
183
+ def self.lib_dir=(path)
184
+ raise RuntimeError, "Project directory not set" unless project_dir
185
+ _assert_path path
186
+ @lib_dir = File.join(project_dir, path)
187
+ end
188
+
189
+ def self.lib_dir
190
+ return @lib_dir if @lib_dir
191
+ File.join(project_dir, 'lib')
192
+ end
193
+
194
+ def self.test_dir=(path)
195
+ raise RuntimeError, "Project directory not set" unless project_dir
196
+ _assert_path path
197
+ @test_dir = File.join(project_dir, path)
198
+ end
199
+
200
+ def self.test_dir
201
+ return @test_dir if @test_dir
202
+ File.join(project_dir, 'test')
203
+ end
204
+
205
+ def self.data_dir=(path)
206
+ raise RuntimeError, "Project directory not set" unless project_dir
207
+ _assert_path path
208
+ @data_dir = File.join(project_dir, path)
209
+ end
210
+
211
+ def self.data_dir
212
+ return @data_dir if @data_dir
213
+ File.join(project_dir, 'data')
214
+ end
215
+
216
+ private
217
+
218
+ def self._assert_path(path)
219
+ unless FileTest.exist?(path)
220
+ raise RuntimeError, "Path doesn't exist: #{path}"
221
+ end
222
+ end
223
+
224
+ #
225
+ # Parse the given arguments for the test loader. Return an OpenStruct with 'mode',
226
+ # 'separate', and 'patterns' attributes.
227
+ #
228
+ def self._parse_options(args)
229
+ opts = OpenStruct.new
230
+ opts.mode = :verbose
231
+ opts.separate = false
232
+ opts.patterns = []
233
+ parser = OptionParser.new do |op|
234
+ op.banner =<<EOF
235
+
236
+ Welcome to the dev-utils/test automatic unit test runner.
237
+ Here are the options:
238
+
239
+ EOF
240
+ op.on('-q', '--quiet', "Quiet mode") { opts.mode = :quiet }
241
+ op.on('-Q', '--very-quiet', "Very quiet mode") { opts.mode = :veryquiet }
242
+ op.on('-v', '--verbose', "Verbose mode (default)") { opts.mode = :verbose }
243
+ op.on('-s', '--separate', "Separate test suite for each test file") { opts.separate = true }
244
+ op.on('-h', '--help', "Show this message") { _show_help(op) }
245
+ op.separator ""
246
+ op.separator " For example, to run only those tests whose filenames include 'remote' or"
247
+ op.separator " 'db', and to run a separate suite for each test file, and to run in quite"
248
+ op.separator " mode:"
249
+ op.separator ""
250
+ op.separator " ruby test/TEST.rb -q remote db -s"
251
+ end
252
+ parser.parse!(args)
253
+ opts.patterns = args.dup
254
+ args.clear
255
+ opts
256
+ end
257
+
258
+ def self._show_help(parser)
259
+ STDERR.puts parser
260
+ exit
261
+ end
262
+
263
+ end # class Test
264
+
265
+ end # module DevUtils
266
+
267
+ =end