require 'rdoc/rdoc' require 'test/unit' ### ### Comment Unit Tests ### Written By Blaine Buxton Copyright 2005-2006 ### ### This is a simple framework that hooks into Test::Unit and RDoc. ### It allows you to write arbitrary bits of code in comments that can be ### executed and then verified vis Test::Unit. It uses the RDoc parser to obtain ### the comments. ### ### Any comments are appreciated ### ### To invoke: ### require 'comment_unit' ### class YourTest < Test::Unit::TestCase ### def self.suite ### suite=super ### suite << RUnitComment.on($0) ### suite ### end ### end ### ### Examples: ### >>> 3 + 4 ### >>= 7 ### ### >>> 42 ### >>= 42 ### ### >>> a='i' ### >>> b=' love ' ### >>> c='michelle' ### >>> a + b + c ### >>= 'i love michelle' ### ### >>> a + ', robot will never die' ### >>= 'i, robot will never die' ### ### >>> a = 5 ### >>> b = 4 + 1 ### >>? a == b ### ### >>> puts 'Comments Ran' class RUnitComment < Test::Unit::TestCase attr_accessor :file_name def self.on(file_name) result = RUnitComment.new(:execute_all_comments) result.file_name = file_name result end def run(result) return if self.file_name.nil? super end def execute_all_comments self.parse_comments unless @top_level @top_level.each_comment do |each| Comments.parse(each).run_tests(self) end end def parse_comments @top_level=RDoc::TopLevel.new(self.file_name) stats=RDoc::Stats.new options=Options.instance options.instance_eval { @tab_width = 2 } content=File.open(file_name, 'r') { |file| file.read } parser = RDoc::ParserFactory.parser_for(@top_level, self.file_name, content, options, stats) parser.scan end end ### ### Domain ### class Comments public def self.parse(comment_string) new(CommentPart.parse(comment_string)) end def run_tests(test_case) test_binding=blank_binding() @parts.inject(nil) {|previous, every| every.run(test_case, previous, test_binding) } end private def new(*arguments) super end def initialize(parts) @parts=parts end end class CommentPart @@mapping=nil @@matcher=nil @@subclasses=[] attr_reader :content public def self.parse(comment_string) previous_part = nil comment_string.inject([]) do |results, line| part = parse_part(line) if (!previous_part.nil? && previous_part.can_combine?(part)) previous_part.combine_with(part) else unless part.nil? results << part previous_part = part end end results end end def to_s "#{self.class}[#{@content}]" end def can_combine?(another) false end def combine_with(another) @content << ';' << another.content end private def self.parse_part(line) match = self.matcher.match(line) if (match) create_part(match[1], match[2]) else nil end end def self.create_part(prefix, content) klass = self.mapping[prefix] klass.new(content) unless klass.nil? end def self.inherited(new_class) super @@mapping=nil @@matcher=nil @@subclasses << new_class end def self.mapping() @@mapping unless @@mapping.nil? @@mapping = @@subclasses.inject({}) do |result, each| result[each.prefix]=each unless each.prefix.nil? result end end def self.matcher() @@matcher unless @@matcher.nil? first=true matcher_string = @@subclasses.inject('\s*(') do |result, each| result << '|' unless first unless each.prefix.nil? result << each.prefix.gsub(/[\?]/) { |each| "\\" + each } first = false end result end matcher_string << ')\s*(.*)' @@matcher = Regexp.new(matcher_string) end def self.prefix() nil end def initialize(content) @content=content end end class ExecutablePart < CommentPart def run(test_case, expected, test_binding=blank_binding()) result = execute(test_binding) assert(test_case, expected, result) result end private def assert(test_case, expected, actual) end def execute(my_binding=blank_binding()) eval(@content, my_binding) end def failure_text() "Failure in #{self.class.prefix} #{@content}" end end class RunPart < ExecutablePart def self.prefix() '>>>' end def can_combine?(another) another.is_a? self.class end end class AssertEqualResultPart < ExecutablePart def self.prefix() '>>=' end private def assert(test_case, expected, actual) test_case.assert_equal(expected, actual, failure_text()) end end class AssertResultPart < ExecutablePart def self.prefix() '>>?' end private def assert(test_case, expected, actual) test_case.assert(actual, failure_text()) end end ### ### Orphan Methods ### def blank_binding() binding().clone() end ### ### Extensions ### class RDoc::Context def each_comment(&proc) super comment_proc=lambda do |each| each.each_comment(&proc) unless each.nil? end each_classmodule(&comment_proc) each_method(&comment_proc) each_attribute(&comment_proc) each_constant(&comment_proc) end end class RDoc::CodeObject def each_comment(&proc) proc.call(self.comment) unless self.comment.nil? || self.comment.empty? end end ### ### TESTS ### if ($0 == __FILE__) ### ### Unit Tests ### class CommentPartTest < Test::Unit::TestCase def test_simple comment = <>>3 + 4 #>>=7 #>>?42 == 42 DOC comments = CommentPart.parse(comment) assert_equal('3 + 4', comments[0].content) assert(comments[0].is_a?(RunPart)) assert_equal(7, comments[0].run(self, nil)) assert_equal('7', comments[1].content) assert(comments[1].is_a?(AssertEqualResultPart)) assert_equal(7, comments[1].run(self, 7)) assert_equal('42 == 42', comments[2].content) assert(comments[2].is_a?(AssertResultPart)) assert_equal(true, comments[2].run(self, nil)) end def test_combine comment = <>>a = 3 #Noise here #>>>b = 4 #>>>a + b #the result is here #>>=7 DOC comments = CommentPart.parse(comment) assert_equal('a = 3;b = 4;a + b', comments[0].content) assert(comments[0].is_a?(RunPart)) assert_equal(7, comments[0].run(self, nil)) assert_equal('7', comments[1].content) assert(comments[1].is_a?(AssertEqualResultPart)) comments[1].run(self, 7) end end class CommentsTest < Test::Unit::TestCase def test_simple() comment = <>>a=3 #>>=3 # #>>>b=4 #>>=4 # #>>>a + b #>>=7 #>>?(a + b) == 7 DOC comments = Comments.parse(comment) comments.run_tests(self) end end ### ###Acceptance tests ### class RDocTest < Test::Unit::TestCase def test_placebo end def self.suite suite=super suite << RUnitComment.on($0) suite end end end