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
+