dev-utils 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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