lib/tryouts.rb in tryouts-0.8.8 vs lib/tryouts.rb in tryouts-2.0.0

- old
+ new

@@ -1,349 +1,434 @@ +#require 'pathname' +#p Pathname(caller.last.split(':').first) +require 'ostruct' -require 'time' -require 'digest/sha1' +unless defined?(TRYOUTS_LIB_HOME) + TRYOUTS_LIB_HOME = File.expand_path File.dirname(__FILE__) +end -require 'attic' -require 'sysinfo' -require 'yaml' +class Tryouts + module VERSION + def self.to_s + load_config + [@version[:MAJOR], @version[:MINOR], @version[:PATCH]].join('.') + end + def self.inspect + load_config + [@version[:MAJOR], @version[:MINOR], @version[:PATCH], @version[:BUILD]].join('.') + end + def self.load_config + require 'yaml' + @version ||= YAML.load_file(File.join(TRYOUTS_LIB_HOME, '..', 'VERSION.yml')) + end + end +end -## NOTE: Don't require rye here so -## we can still run tryouts on the -## development version. -begin; require 'json'; rescue LoadError; end # json may not be installed +class Tryouts + @debug = false + @container = Class.new + @cases = [] + @sysinfo = nil + class << self + attr_accessor :debug, :container + attr_reader :cases + + def sysinfo + require 'sysinfo' + @sysinfo ||= SysInfo.new + @sysinfo + end + + def debug?() @debug == true end + + def run_all *paths + batches = paths.collect do |path| + run path + end + + all, skipped_tests, failed_tests = 0, 0, 0 + skipped_batches, failed_batches = 0, 0 + + msg 'Ruby %s @ %-40s' % [RUBY_VERSION, Time.now], $/ + + batches.each do |batch| + if !batch.run? + skipped_batches += 1 + status = "SKIP" + elsif batch.failed? + failed_batches += 1 + status = Console.color(:red, "FAIL").bright + else + status = Console.color(:green, "PASS").bright + end + + path = batch.path.gsub(/#{Dir.pwd}\/?/, '') + + msg '%-60s %s' % [path, status] + batch.each do |t| + if t.failed? && failed_tests == 0 + #msg Console.reverse(" %-60s" % 'Errors') + end + + all += 1 + skipped_tests += 1 unless t.run? -GYMNASIUM_HOME = File.join(Dir.pwd, '{tryouts,try}') ## also check try (for rye) -GYMNASIUM_GLOB = File.join(GYMNASIUM_HOME, '**', '*_tryouts.rb') + if t.failed? + msg if (failed_tests += 1) == 1 + msg Console.reverse(' %-58s ' % [t.desc.to_s]) + msg t.test.inspect, t.exps.inspect + msg Console.color(:red, t.failed.join($/)), $/ + end + end + end + msg + if all > 0 + suffix = 'tests passed' + suffix << " (#{skipped_tests} skipped)" if skipped_tests > 0 + msg cformat(all-failed_tests-skipped_tests, all-skipped_tests, suffix) if all-skipped_tests > 0 + end + if batches.size > 1 + if batches.size-skipped_batches > 0 + suffix = "batches passed" + suffix << " (#{skipped_batches} skipped)" if skipped_batches > 0 + msg cformat(batches.size-skipped_batches-failed_batches, batches.size-skipped_batches, suffix) + end + end + + failed_tests # 0 means success + end + + def cformat(*args) + Console.bright '%3d of %d %s' % args + end + + def run path + batch = parse path + batch.run + batch + end + + def parse path + #debug "Loading #{path}" + lines = File.readlines path + skip_ahead = 0 + batch = TestBatch.new path, lines + lines.size.times do |idx| + skip_ahead -= 1 and next if skip_ahead > 0 + line = lines[idx].chomp + #debug('%-4d %s' % [idx, line]) + if expectation? line + offset = 0 + exps = Section.new(path, idx+1) + exps << line.chomp + while (idx+offset < lines.size) + offset += 1 + this_line = lines[idx+offset] + break if ignore?(this_line) + if expectation?(this_line) + exps << this_line.chomp + skip_ahead += 1 + end + exps.last += 1 + end + + offset = 0 + buffer, desc = Section.new(path), Section.new(path) + test = Section.new(path, idx) # test start the line before the exp. + blank_buffer = Section.new(path) + while (idx-offset >= 0) + offset += 1 + this_line = lines[idx-offset].chomp + buffer.unshift this_line if ignore?(this_line) + if comment?(this_line) + buffer.unshift this_line + end + if test?(this_line) + test.unshift(*buffer) && buffer.clear + test.unshift this_line + end + if test_begin?(this_line) + while test_begin?(lines[idx-(offset+1)].chomp) + offset += 1 + buffer.unshift lines[idx-offset].chomp + end + end + if test_begin?(this_line) || idx-offset == 0 || expectation?(this_line) + adjust = expectation?(this_line) ? 2 : 1 + test.first = idx-offset+buffer.size+adjust + desc.unshift *buffer + desc.last = test.first-1 + desc.first = desc.last-desc.size+1 + # remove empty lines between the description + # and the previous expectation + while !desc.empty? && desc[0].empty? + desc.shift + desc.first += 1 + end + break + end + end + + batch << TestCase.new(desc, test, exps) + end + end - -# = Tryouts -# -# This class has three purposes: -# * It represents the Tryouts object which is a group of Tryout objects. -# * The tryouts and dreams DSLs are executed within its namespace. In general the -# class methods are the handlers for the DSL syntax (some instance getter methods -# are modified to support DSL syntax by acting like setters when given arguments) -# * It stores all known instances of Tryouts objects in a class variable @@instances. -# -# ==== Are you ready to run some drills? -# -# May all your dreams come true! -# -class Tryouts - # = Exception - # A generic exception which all other Tryouts exceptions inherit from. - class Exception < RuntimeError; end - # = BadDreams - # Raised when there is a problem loading or parsing a Tryouts::Drill::Dream object - class BadDream < Tryouts::Exception; end - class TooManyArgs < Tryouts::Exception; end - class NoDrillType < Tryouts::Exception - attr_accessor :tname - def initialize(t); @tname = t; end - def message - vdt = Tryouts::Drill.valid_dtypes - "Tryout '#{@tname}' has no drill type. Should be: #{vdt.join(', ')}" + batch end - end - VERSION = "0.8.8" - - require 'tryouts/mixins' - require 'tryouts/tryout' - require 'tryouts/drill' - - autoload :Stats, 'tryouts/stats' - - unless defined?(HASH_TYPE) - if RUBY_VERSION =~ /1.8/ - require 'tryouts/orderedhash' - HASH_TYPE = Tryouts::OrderedHash - else - HASH_TYPE = Hash + def print str + STDOUT.print str + STDOUT.flush end - end - - # An Array of +_tryouts.rb+ file paths that have been loaded. - @@loaded_files = [] - # An Hash of Tryouts instances stored under the name of the Tryouts subclass. - @@instances = HASH_TYPE.new - # An instance of SysInfo - @@sysinfo = nil - - @@debug = false - @@verbose = 0 - # This will be true if any error occurred during any of the drills or parsing. - @@failed = false - - def self.debug?; @@debug; end - def self.enable_debug; @@debug = true; end - def self.disable_debug; @@debug = false; end - - def self.verbose; @@verbose; end - def self.verbose=(v); @@verbose = (v == true) ? 1 : v; end - - def self.failed?; @@failed; end - def self.failed=(v); @@failed = v; end - - # Returns +@@instances+ - def self.instances; @@instances; end - # Returns +@@sysinfo+ - def self.sysinfo - @@sysinfo = SysInfo.new if @@sysinfo.nil? - @@sysinfo - end - - # The name of this group of Tryout objects - attr_accessor :group - # A Symbol representing the default drill type. One of: :cli, :api - attr_accessor :dtype - # An Array of file paths which populated this instance of Tryouts - attr_accessor :paths - # An Array of Tryout objects - attr_accessor :tryouts - # A Symbol representing the command taking part in the tryouts. For @dtype :cli only. - attr_accessor :command - # A Symbol representing the name of the library taking part in the tryouts. For @dtype :api only. - attr_accessor :library - # An Array of exceptions that were raised during the tryouts that were not captured by a drill. - attr_reader :errors - - def initialize(group=nil) - @group = group || "Default Group" - @tryouts = HASH_TYPE.new - @paths, @errors = [], [] - @command = nil - end - - # Populate this Tryouts from a block. The block should contain calls to - # the external DSL methods: tryout, command, library, group - def from_block(b, &inline) - instance_eval &b - end - - # Execute Tryout#report for each Tryout in +@tryouts+ - def report - successes = [] - @tryouts.each_pair { |n,to| successes << to.report } - puts $/, "All your dreams came true" unless successes.member?(false) - end - - # Execute Tryout#run for each Tryout in +@tryouts+ - def run; @tryouts.each_pair { |n,to| to.run }; end - - # Add a shell command to Rye::Cmd and save the command name - # in @@commands so it can be used as the default for drills - def command(name=nil, path=nil) - return @command if name.nil? - require 'rye' - @command = name.to_sym - @dtype = :cli - Rye::Cmd.module_eval do - define_method(name) do |*args| - cmd(path || name, *args) + + def msg *msg + STDOUT.puts *msg + end + + def err *msg + msg.each do |line| + STDERR.puts Console.color :red, line end end - @command - end - # Calls Tryouts#command on the current instance of Tryouts - # - # NOTE: this is a standalone DSL-syntax method. - def self.command(*args) - @@instances.last.command(*args) - end - - # Require +name+. If +path+ is supplied, it will "require path". - # * +name+ The name of the library in question (required). Stored as a Symbol to +@library+. - # * +path+ Add a path to the front of $LOAD_PATH (optional). Use this if you want to load - # a specific copy of the library. Otherwise, it loads from the system path. If the path - # in specified in multiple arguments they are joined and expanded. - # - # library '/an/absolute/path' - # library __FILE__, '..', 'lib' - # - def library(name=nil, *path) - return @library if name.nil? - @library, @dtype = name.to_sym, :api - path = File.expand_path(File.join(*path)) - $LOAD_PATH.unshift path unless path.nil? - begin - require @library.to_s - rescue LoadError => ex - newex = Tryouts::Exception.new(ex.message) - trace = ex.backtrace - trace.unshift @paths.last - newex.set_backtrace trace - @errors << newex - Tryouts.failed = true - rescue SyntaxError, Exception, TypeError, - RuntimeError, NoMethodError, NameError => ex - @errors << ex - Tryouts.failed = true + + def debug *msg + STDERR.puts *msg if @debug end - end - # Calls Tryouts#library on the current instance of Tryouts - # - # NOTE: this is a standalone DSL-syntax method. - def self.library(*args) - @@instances.last.library(*args) - end - - def group(name=nil) - return @group if name.nil? - @group = name unless name.nil? - @group - end - # Raises a Tryouts::Exception. +group+ is not support in the standalone syntax - # because the group name is taken from the name of the class. See inherited. - # - # NOTE: this is a standalone DSL-syntax method. - def self.group(*args) - raise "Group is already set: #{@@instances.last.group}" - end - - # Create a new Tryout object and add it to the list for this Tryouts class. - # * +name+ is the name of the Tryout - # * +dtype+ is the default drill type for the Tryout. - # * +command+ when type is :cli, this is the name of the Rye::Box method that we're testing. Otherwise ignored. - # * +b+ is a block definition for the Tryout. See Tryout#from_block - # - # NOTE: This is a DSL-only method and is not intended for OO use. - def tryout(name, dtype=nil, command=nil, &block) - return if name.nil? - dtype ||= @dtype - command ||= @command if dtype == :cli - raise NoDrillType, name if dtype.nil? + def eval(str, path, line) + begin + Kernel.eval str, @container.send(:binding), path, line + rescue SyntaxError, LoadError => ex + Tryouts.err Console.color(:red, ex.message), + Console.color(:red, ex.backtrace.first) + nil + end + end - to = find_tryout(name, dtype) - if to.nil? - to = Tryouts::Tryout.new(name, dtype, command) - @tryouts[name] = to + private + + def expectation? str + !ignore?(str) && str.strip.match(/^\#\s*=>/) end - # Process the rest of the DSL - begin - to.from_block block if block - rescue SyntaxError, LoadError, Exception, TypeError, - RuntimeError, NoMethodError, NameError => ex - @errors << ex - Tryouts.failed = true + def comment? str + !str.strip.match(/^\#+/).nil? && !expectation?(str) end - to - end - # Calls Tryouts#tryout on the current instance of Tryouts - # - # NOTE: this is a standalone DSL-syntax method. - def self.tryout(*args, &block) - @@instances.last.tryout(*args, &block) - end + + def test? str + !ignore?(str) && !expectation?(str) && !comment?(str) + end + + def ignore? str + str.to_s.strip.chomp.empty? + end + + def test_begin? str + ret = !str.strip.match(/^\#+\s*TEST/i).nil? || + !str.strip.match(/^\#\#+[\s\w]+/i).nil? + ret + end - # Find matching Tryout objects by +name+ and filter by - # +dtype+ if specified. Returns a Tryout object or nil. - def find_tryout(name, dtype=nil) - by_name = @tryouts.values.select { |t| t.name == name } - by_name = by_name.select { |t| t.dtype == dtype } if dtype - by_name.first # by_name is an Array. We just want the Object. + end - # This method does nothing. It provides a quick way to disable a tryout. - # - # NOTE: This is a DSL-only method and is not intended for OO use. - def xtryout(*args, &block); end - # This method does nothing. It provides a quick way to disable a tryout. - # - # NOTE: this is a standalone DSL-syntax method. - def self.xtryout(*args, &block); end - - # Returns +@tryouts+. - # - # Also acts as a stub for Tryouts#tryout in case someone - # specifies "tryouts 'name' do ..." in the DSL. - def tryouts(*args, &block) - return tryout(*args, &block) unless args.empty? - @tryouts - end - # An alias for Tryouts.tryout. - def self.tryouts(*args, &block) - tryout(args, &block) - end - - # This method does nothing. It provides a quick way to disable a tryout. - # - # NOTE: This is a DSL-only method and is not intended for OO use. - def xtryouts(*args, &block); end - # This method does nothing. It provides a quick way to disable a tryout. - # - # NOTE: this is a standalone DSL-syntax method. - def self.xtryouts(*args, &block); end - - - # Parse a +_tryouts.rb+ file. See Tryouts::CLI::Run for an example. - # - # NOTE: this is an OO syntax method - def self.parse_file(fpath) - raise "No such file: #{fpath}" unless File.exists?(fpath) - file_content = File.read(fpath) - to = Tryouts.new - begin - to.paths << fpath - to.instance_eval file_content, fpath - # After parsing the DSL, we'll know the group name. - # If a Tryouts object already exists for that group - # we'll use that instead and re-parse the DSL. - if @@instances.has_key? to.group - to = @@instances[to.group] - to.instance_eval file_content, fpath + class TestBatch < Array + class Container + def metaclass + class << self; end end - rescue SyntaxError, LoadError, Exception, TypeError, - RuntimeError, NoMethodError, NameError => ex - to.errors << ex - Tryouts.failed = true - # It's helpful to display the group name - file_content.match(/^group (.+?)$/) do |x,t| - # We use eval as a quick cheat so we don't have - # to parse all the various kinds of quotes. - to.group = eval x.captures.first + end + attr_reader :path + attr_reader :failed + attr_reader :lines + def initialize(p,l) + @path, @lines = p, l + @container = Container.new.metaclass + @run = false + end + def run + return if empty? + setup + ret = self.select { |tc| !tc.run } # select failed + @failed = ret.size + @run = true + clean + !failed? + end + def failed? + !@failed.nil? && @failed > 0 + end + def setup + return if empty? + start = first.desc.nil? ? first.test.first : first.desc.first-1 + Tryouts.eval lines[0..start-1].join, path, 0 if start > 0 + end + def clean + return if empty? + last = first.exps.last+1 + if last < lines.size + Tryouts.eval lines[last..-1].join, path, last end end - @@instances[to.group] = to - to + def run? + @run + end end - - # Run all Tryout objects in +@tryouts+ - # - # NOTE: this is an OO syntax method - def self.run - @@instances.each_pair do |group, inst| - inst.tryouts.each_pair do |name,to| - to.run - to.report - end + class TestCase + attr_reader :desc, :test, :exps, :failed, :passed + def initialize(d,t,e) + @desc, @test, @exps, @path = d,t,e end + def inspect + [@desc.inspect, @test.inspect, @exps.inspect].join + end + def to_s + [@desc.to_s, @test.to_s, @exps.to_s].join + end + def run + Tryouts.debug '%s:%d' % [@test.path, @test.first] + Tryouts.debug inspect, $/ + test_value = Tryouts.eval @test.to_s, @test.path, @test.first + @passed, @failed = [], [] + exps.each_with_index { |exp,idx| + exp =~ /\#+\s*=>\s*(.+)$/ + exp_value = Tryouts.eval($1, @exps.path, @exps.first+idx) + ret = test_value == exp_value + if ret + @passed << ' %s == %s' % [test_value.inspect, exp_value.inspect] + else + @failed << ' %s != %s' % [test_value.inspect, exp_value.inspect] + end + ret + } + Tryouts.debug + @failed.empty? + end + def run? + !@failed.nil? + end + def failed? + !@failed.nil? && !@failed.empty? + end + private + def create_proc str, path, line + eval("Proc.new {\n #{str}\n}", binding, path, line) + end end - - # Called when a new class inherits from Tryouts. This creates a new instance - # of Tryouts, sets group to the name of the new class, and adds the instance - # to +@@instances+. - # - # NOTE: this is a standalone DSL-syntax method. - def self.inherited(klass) - to = @@instances[ klass ] - to ||= Tryouts.new - to.paths << __FILE__ - to.group = klass - @@instances[to.group] = to + class Section < Array + attr_accessor :path, :first, :last + def initialize path, start=0 + @path = path + @first, @last = start, start + end + def range + @first..@last + end + def inspect + range.to_a.zip(self).collect do |line| + "%-4d %s\n" % line + end.join + end + def to_s + self.join($/) + end end + - - ##--- - ## Is this wacky syntax useful for anything? - ## t2 :set . - ## run = "poop" - ## def self.t2(*args) - ## OpenStruct.new - ## end - ##+++ - + module Console + + # ANSI escape sequence numbers for text attributes + ATTRIBUTES = { + :normal => 0, + :bright => 1, + :dim => 2, + :underline => 4, + :blink => 5, + :reverse => 7, + :hidden => 8, + :default => 0, + }.freeze unless defined? ATTRIBUTES + # ANSI escape sequence numbers for text colours + COLOURS = { + :black => 30, + :red => 31, + :green => 32, + :yellow => 33, + :blue => 34, + :magenta => 35, + :cyan => 36, + :white => 37, + :default => 39, + :random => 30 + rand(10).to_i + }.freeze unless defined? COLOURS + + # ANSI escape sequence numbers for background colours + BGCOLOURS = { + :black => 40, + :red => 41, + :green => 42, + :yellow => 43, + :blue => 44, + :magenta => 45, + :cyan => 46, + :white => 47, + :default => 49, + :random => 40 + rand(10).to_i + }.freeze unless defined? BGCOLOURS + + module InstanceMethods + def bright + Console.bright(self) + end + def reverse + Console.reverse(self) + end + def color(col) + Console.color(col, self) + end + def att(col) + Console.att(col, self) + end + def bgcolor(col) + Console.bgcolor(col, self) + end + end + + def self.bright(str) + str = [style(ATTRIBUTES[:bright]), str, default_style].join + str.extend Console::InstanceMethods + str + end + def self.reverse(str) + str = [style(ATTRIBUTES[:reverse]), str, default_style].join + str.extend Console::InstanceMethods + str + end + def self.color(col, str) + str = [style(COLOURS[col]), str, default_style].join + str.extend Console::InstanceMethods + str + end + def self.att(name, str) + str = [style(ATTRIBUTES[name]), str, default_style].join + str.extend Console::InstanceMethods + str + end + def self.bgcolor(col, str) + str = [style(ATTRIBUTES[col]), str, default_style].join + str.extend Console::InstanceMethods + str + end + private + def self.style(*att) + # => \e[8;34;42m + "\e[%sm" % att.join(';') + end + def self.default_style + style(ATTRIBUTES[:default], ATTRIBUTES[:COLOURS], ATTRIBUTES[:BGCOLOURS]) + end + end + end +