From: Florian Gross Date: 2004-10-12T20:39:51+09:00 Subject: Re: documentation as source --------------010501010308060408060301 Content-Type: text/plain; charset=us-ascii; format=flowed Content-Transfer-Encoding: 7bit Bil Kleb wrote: > Randy W. Sims wrote: > >> Are there any Ruby modules that allow documentation to act as source >> code? > > require 'test/unit' > > Then code test-first and you'll have executable documentation. I've coded up test-extract which lets you embed some of your test-cases cloaked as sample code into your RDoc strings. It looks like this in practice: > # Creates a Regexp which matches a literal string. In this > # string any special regular expression meta-characters will > # be escaped automatically. > # > # # This creates a Regexp which will match 3 "foo"s. > # re = Regexp::English.literal("foo" * 3) > # re.match("foofoofoo")[0] # => "foofoofoo" > def literal(text); Node::Literal.new(text); end (Sample from Regexp::English) > # Unfreeze a frozen Object. You will be able to make > # changes to the object again. > # > # obj = "Hello World".freeze > # obj.frozen? # => true > # obj.unfreeze > # obj.frozen? # => false > # obj.sub!("World", "You!") > # obj # => "Hello You!" > def unfreeze > if $SAFE > 0 > raise(SecurityError, "Insecure operation `unfreeze' at level #{$SAFE}") > end > > return self if direct_value? > > self.internal.flags &= ~RubyInternal::FL_FREEZE > return self > end (Sample from Evil-Ruby) I've attached it to this email. > Code test-first with a partner, and you'll have /sane/ executable > documentation. Heh, I have not done this yet, but I've noticed that code design generally turn out better if you do it via pair programming. > When test/unit2 appears, tests will become even more like documentation. I'm interested: What will it change? > I've played with Literate Programming for over a decade and found > that it nearly always violates the DRY principal. As a result, it > eventually gets out of sync and begins to lie. Would you consider the code samples from above Literate Programming? > Regards, > Bil Kleb, Hampton, Virginia More regards, Florian Gross --------------010501010308060408060301 Content-Type: text/plain; name="extract.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="extract.rb" require 'test/unit/testcase' require 'test/unit/ui/console/testrunner' # Example isn't another way to teach, it is the only way to teach. # --Albert Einstein class Extracter def self.process(fn) new(File.read(fn)) end def initialize(content) comment_block_re = /((?:^\s*?(?:#.*?)?\n)+)/m component_re = /\s*(?:class|def|module|alias)\s+:?([^\s()]+)?/ blocks = content.scan(/#{comment_block_re}#{component_re}/) test_suite = Class.new(Test::Unit::TestCase) has_test = false blocks.each do |(comment, component)| code_in_doc_re = /^(\s*# +(?:.*?)$)/ tests = comment.scan(code_in_doc_re) body = tests.map do |test| test.map do |raw_line| line = raw_line.sub(/^\s*#\s{0,3}/, "") if md = /(.*?)#\s*(=>|~>|raises?)\s*(.*?)$/.match(line) new_line, type, result = *md.captures new_line.strip! case type when "=>" ["begin", " assert_equal(#{result}, #{new_line})", "rescue => err", " assert_equal(#{result.inspect}, (#{new_line}).inspect)", "end"].join("\n") when "~>", "raise", "raises" "assert_raises(Object.const_get(#{result.inspect})) { #{new_line} }" end else line end end.join("\n") end.join("\n") unless component if $DEBUG STDERR.puts "Can't get name for this code:", body.gsub(/(?:\r?\n){2}/, "\n") end component = test.hash.abs end if body and not body.empty? has_test = true test_suite.class_eval %{ def #{test_method_name(component)} #{body} end } end end if not has_test test_suite.class_eval do def test_main; end end end Test::Unit::UI::Console::TestRunner.new(test_suite).start end def test_method_name(component) result = "test_#{component}" { "+" => "op_plus", "-" => "op_minus", "+@" => "op_plus_self", "-@" => "op_minus_self", "*" => "op_mul", "**" => "op_pow", "/" => "op_div", "%" => "op_mod", "<<" => "op_lshift", ">>" => "op_rshift", "~" => "op_tilde", "<=>" => "op_cmp", "<" => "op_lt", ">" => "op_gt", "==" => "op_equal", "<=" => "op_lt_eq", ">=" => "op_gt_eq", "===" => "op_case_eq", "=~" => "op_apply", "|" => "op_or", "&" => "op_and", "^" => "op_xor", "[]" => "op_fetch", "[]=" => "op_store" }.each do |(what, by)| result.gsub!(what, by) end return result end end if __FILE__ == $0 file = ARGV.shift load(file) Extracter.process(file) end --------------010501010308060408060301--