02: class T3
03: def test_1
04: a = 0
05: 1/a
06: end
07:
08: def test_2
09: p 'test_2'
10: end
11:
12: end
13:
14: t = T3.new
15: begin
16: t.test_1
17: rescue
18: breakpoint
19: end
20: t.test_2
因为test_1用零除,第18行会被执行,导致如下的irb窗口被弹出。弹出的irb已经包含了执行中断时的所用信息,包括当时的变量,t。我们可以用local_variables查看变量。我们可以用source_lines查看中断前后的代码。我们甚至可以修改test3.rb,然后load 'test3.rb',然后恢复执行。
为什么要在测试代码里做这些?呵呵。想象一下我们用Watir测试一个复杂的网络应用。在执行20分钟后的第30步时Watir找不到网页上一个按钮了。如果这时这个irb窗口出现,我们通过查看中断时的上下文,发现原来是用来搜索按钮的字串错了。我们于是在测试代码里改动这字串,"reload"改动后的代码或者顺手替Watir点击那个按钮,然后"exit"这个irb窗口,让测试代码继续执行。也就是说,代码出错 -》中断执行 -》调试 -》继续执行。我们都知道,出错时立刻排错的效率可比重头再来高多了。谁都不想重复前29步,等上20分钟。
听上去不错吧?不过俺们是程序员,岂能满足与此。于是新要求来了:如果等到出错时再加begin...rescue breakpoint..end就太晚了。如果没行都加就太麻烦了。能不能在写测试代码时不加任何额外代码,只在一个地方控制一下就行了嗫?比如说,上面的代码改成这样:
02: class T3
03: def test_1
04: a = 0 05: 1/a
06: end 07:
08: def test_2
09: p 'test_2' 10: end 11:
12: monitor /test/ 13: end 14:
15: t = T3.new
16: t.test_1
17: t.test_2
注意第12行。我们添加一个新函数,凡是名字匹配/test/的函数都会在出错的时候自动弹出breakpoint的irb窗口。
这样就非常方便了。当然,我们也允许monitor :test_1这样粒度更细的语法。这样我们不用多加代码,还可以控制
到底什么时候允许breakpoint生效(比如说,在一个全局配置关闭时让monitor什么都不做)。嗯,AOP的老大们笑
了。用惯Lisp Macro的老大们也笑了。让我们看看Ruby怎么实现吧。同样非常简单。下面是一段基本的代码。我去掉了
许多无关的细节。我们打开内置的类Module,加入一个关键的instance method, monitor。这个函数,monitor, 把传给它的函数用begin..rescue..breakpoint..end
包装起来。于是,我们的函数自动拥有了breakpoint的功能。简单得令人发指啊。
require 'breakpoint' class Module
def match?(element, array) array.each do |e| e = e.to_s if e.instance_of?(Symbol) return true if element.match(e) end return false end
def monitored_method_ids(*method_names) return self.instance_methods(false).inject([]) do |result, method| result.push(method.to_sym) if match?(method, method_names) result end end
def monitor(*args)
monitored_method_ids(*args).each do |method| puts "starting to eval: #{method.to_i} to #{method.to_s} " module_eval <<-EVAL_END
alias_method :__#{method.to_i}__, :#{method.to_s} puts '===> #{__FILE__}' def #{method.to_s} begin puts "before running __#{method.to_s}__" __#{method.to_i}__ rescue => e
breakpoint
end
end EVAL_END end
end
end
思考题: