# Bacon -- small RSpec clone. # # "Truth will sooner come out from error than from confusion." ---Francis Bacon # # Copyright (C) 2011 Eloy DurĂ¡n eloy.de.enige@gmail.com # Copyright (C) 2007 - 2011 Christian Neukirchen # # Bacon is freely distributable under the terms of an MIT-style license. # See COPYING or http://www.opensource.org/licenses/mit-license.php. module Bacon VERSION = "1.3" Counter = Hash.new(0) ErrorLog = "" Shared = Hash.new { |_, name| raise NameError, "no such context: #{name.inspect}" } RestrictName = // unless defined? RestrictName RestrictContext = // unless defined? RestrictContext Backtraces = true unless defined? Backtraces module RubyMineOutput @@entered = false @@description = nil @@specification = nil @@started = nil def handle_specification_begin(name) unless @@entered puts "##teamcity[enteredTheMatrix timestamp = '#{java_time}']\n\n" @@entered = true end @@specification = name puts "##teamcity[testSuiteStarted timestamp = '#{java_time}' name = '#{escape_message(name)}']\n\n" end def handle_specification_end puts "##teamcity[testSuiteFinished timestamp = '#{java_time}' name = '#{escape_message(@@specification)}']\n\n" if Counter[:context_depth] == 1 end def handle_requirement_begin(description) @@description = description @@started = Time.now puts "##teamcity[testStarted timestamp = '#{java_time}' captureStandardOutput = 'true' name = '#{escape_message(description)}']\n\n" end def handle_requirement_end(error) if !error.empty? puts "##teamcity[testFailed timestamp = '#{java_time}' message = '#{escape_message(error)}' name = '#{escape_message(@@description)}']\n\n" end duration = ((Time.now - @@started) * 1000).to_i puts "##teamcity[testFinished timestamp = '#{java_time}' duration = '#{duration}' name = '#{escape_message(@@description)}']\n\n" end def handle_summary print ErrorLog if Backtraces puts "%d specifications (%d requirements), %d failures, %d errors" % Counter.values_at(:specifications, :requirements, :failed, :errors) end def spaces " " * (Counter[:context_depth] - 1) end def java_time convert_time_to_java_simple_date(Time.now) end def escape_message(message) copy_of_text = String.new(message) copy_of_text.gsub!(/\|/, "||") copy_of_text.gsub!(/'/, "|'") copy_of_text.gsub!(/\n/, "|n") copy_of_text.gsub!(/\r/, "|r") copy_of_text.gsub!(/\]/, "|]") copy_of_text.gsub!(/\[/, "|[") begin copy_of_text.encode!('UTF-8') if copy_of_text.respond_to? :encode! copy_of_text.gsub!(/\u0085/, "|x") # next line copy_of_text.gsub!(/\u2028/, "|l") # line separator copy_of_text.gsub!(/\u2029/, "|p") # paragraph separator rescue # it is not an utf-8 compatible string :( end copy_of_text end def convert_time_to_java_simple_date(time) gmt_offset = time.gmt_offset gmt_sign = gmt_offset < 0 ? "-" : "+" gmt_hours = gmt_offset.abs / 3600 gmt_minutes = gmt_offset.abs % 3600 millisec = time.usec == 0 ? 0 : time.usec / 1000 #Time string in Java SimpleDateFormat sprintf("#{time.strftime("%Y-%m-%dT%H:%M:%S.")}%03d#{gmt_sign}%02d%02d", millisec, gmt_hours, gmt_minutes) end end module SpecDoxOutput def handle_specification_begin(name) puts spaces + name end def handle_specification_end puts if Counter[:context_depth] == 1 end def handle_requirement_begin(description) print "#{spaces} - #{description}" end def handle_requirement_end(error) puts error.empty? ? "" : " [#{error}]" end def handle_summary print ErrorLog if Backtraces puts "%d specifications (%d requirements), %d failures, %d errors" % Counter.values_at(:specifications, :requirements, :failed, :errors) end def spaces " " * (Counter[:context_depth] - 1) end end module TestUnitOutput def handle_specification_begin(name); end def handle_specification_end ; end def handle_requirement_begin(description) end def handle_requirement_end(error) if error.empty? print "." else print error[0..0] end end def handle_summary puts "", "Finished in #{Time.now - @timer} seconds." puts ErrorLog if Backtraces puts "%d tests, %d assertions, %d failures, %d errors" % Counter.values_at(:specifications, :requirements, :failed, :errors) end end module ColorizedOutput GREEN = "\033[0;32m" RED = "\033[0;31m" RESET = "\033[00m" def handle_specification_begin(name); end def handle_specification_end ; end def handle_requirement_begin(description) end def handle_requirement_end(error) if error.empty? print "#{GREEN}.#{RESET}" else print "#{RED}#{error[0..0]}#{RESET}" end end def handle_summary puts "" puts "", ErrorLog if Backtraces && ! ErrorLog.empty? duration = "%0.2f" % (Time.now - @timer) puts "", "Finished in #{duration} seconds." color = GREEN if Counter[:errors] > 0 || Counter[:failed] > 0 color = RED end puts "#{color}%d tests, %d assertions, %d failures, %d errors#{RESET}" % Counter.values_at(:specifications, :requirements, :failed, :errors) puts "" end end module FastOutput def handle_specification_begin(name); end def handle_specification_end; end def handle_requirement_begin(description); end def handle_requirement_end(error) return if error.empty? print error[0..0] end def handle_summary puts "", "Finished in #{Time.now - @timer} seconds." puts ErrorLog if Backtraces puts "%d tests, %d assertions, %d failures, %d errors" % Counter.values_at(:specifications, :requirements, :failed, :errors) end end module TapOutput @@count = 1 @@describe = "" def handle_specification_begin(name) @@describe << "#{name} " end def handle_specification_end @@describe = "" end def handle_requirement_begin(description) @description = @@describe + description @description.sub!(/^[#\s]+/, "") ErrorLog.replace "" end def handle_requirement_end(error) if error.empty? puts "ok %-3d - %s" % [@@count, @description] else puts "not ok %d - %s: %s" % [@@count, @description, error] puts ErrorLog.strip.gsub(/^/, '# ') if Backtraces end @@count += 1 end def handle_summary puts "1..#{Counter[:specifications]}" puts "# %d tests, %d assertions, %d failures, %d errors" % Counter.values_at(:specifications, :requirements, :failed, :errors) end end module KnockOutput def handle_specification_begin(name); end def handle_specification_end ; end def handle_requirement_begin(description) @description = description ErrorLog.replace "" end def handle_requirement_end(error) if error.empty? puts "ok - %s" % [@description] else puts "not ok - %s: %s" % [@description, error] puts ErrorLog.strip.gsub(/^/, '# ') if Backtraces end end def handle_summary; end end Outputs = { 'spec_dox' => SpecDoxOutput, 'fast' => FastOutput, 'test_unit' => TestUnitOutput, 'tap' => TapOutput, 'knock' => KnockOutput, 'rubymine' => RubyMineOutput, 'colorized' => ColorizedOutput, } extend(Outputs[ENV['output']] || SpecDoxOutput) class Error < RuntimeError attr_accessor :count_as def initialize(count_as, message) @count_as = count_as super message end end class Specification attr_reader :description def initialize(context, description, block, before_filters, after_filters) @context, @description, @block = context, description, block @before_filters, @after_filters = before_filters.dup, after_filters.dup @postponed_blocks_count = 0 @ran_spec_block = false @ran_after_filters = false @exception_occurred = false @error = "" end def postponed? @postponed_blocks_count != 0 end def run_before_filters execute_block { @before_filters.each { |f| @context.instance_eval(&f) } } end def run_spec_block @ran_spec_block = true # If an exception occurred, we definitely don't need to perform the actual spec anymore unless @exception_occurred execute_block { @context.instance_eval(&@block) } end finish_spec unless postponed? end def run_after_filters @ran_after_filters = true execute_block { @after_filters.each { |f| @context.instance_eval(&f) } } end def run Bacon.handle_requirement_begin(@description) Counter[:depth] += 1 run_before_filters @number_of_requirements_before = Counter[:requirements] run_spec_block unless postponed? end def schedule_block(seconds, &block) # If an exception occurred, we definitely don't need to schedule any more blocks unless @exception_occurred @postponed_blocks_count += 1 performSelector("run_postponed_block:", withObject:block, afterDelay:seconds) end end def postpone_block(timeout = 1, &block) # If an exception occurred, we definitely don't need to schedule any more blocks unless @exception_occurred if @postponed_block raise "Only one indefinite `wait' block at the same time is allowed!" else @postponed_blocks_count += 1 @postponed_block = block performSelector("postponed_block_timeout_exceeded", withObject:nil, afterDelay:timeout) end end end def postpone_block_until_change(object_to_observe, key_path, timeout = 1, &block) # If an exception occurred, we definitely don't need to schedule any more blocks unless @exception_occurred if @postponed_block raise "Only one indefinite `wait' block at the same time is allowed!" else @postponed_blocks_count += 1 @postponed_block = block @observed_object_and_key_path = [object_to_observe, key_path] object_to_observe.addObserver(self, forKeyPath:key_path, options:0, context:nil) performSelector("postponed_change_block_timeout_exceeded", withObject:nil, afterDelay:timeout) end end end def observeValueForKeyPath(key_path, ofObject:object, change:_, context:__) resume end def postponed_change_block_timeout_exceeded remove_observer! postponed_block_timeout_exceeded end def remove_observer! if @observed_object_and_key_path object, key_path = @observed_object_and_key_path object.removeObserver(self, forKeyPath:key_path) @observed_object_and_key_path = nil end end def postponed_block_timeout_exceeded cancel_scheduled_requests! execute_block { raise Error.new(:failed, "timeout exceeded: #{@context.name} - #{@description}") } @postponed_blocks_count = 0 finish_spec end def resume NSObject.cancelPreviousPerformRequestsWithTarget(self, selector:'postponed_block_timeout_exceeded', object:nil) NSObject.cancelPreviousPerformRequestsWithTarget(self, selector:'postponed_change_block_timeout_exceeded', object:nil) remove_observer! block, @postponed_block = @postponed_block, nil run_postponed_block(block) end def run_postponed_block(block) # If an exception occurred, we definitely don't need execute any more blocks execute_block(&block) unless @exception_occurred @postponed_blocks_count -= 1 unless postponed? if @ran_after_filters exit_spec elsif @ran_spec_block finish_spec else run_spec_block end end end def finish_spec if !@exception_occurred && Counter[:requirements] == @number_of_requirements_before # the specification did not contain any requirements, so it flunked execute_block { raise Error.new(:missing, "empty specification: #{@context.name} #{@description}") } end run_after_filters exit_spec unless postponed? end def cancel_scheduled_requests! NSObject.cancelPreviousPerformRequestsWithTarget(@context) NSObject.cancelPreviousPerformRequestsWithTarget(self) end def exit_spec cancel_scheduled_requests! Counter[:depth] -= 1 Bacon.handle_requirement_end(@error) @context.specification_did_finish(self) end def execute_block begin yield rescue Object => e @exception_occurred = true if e.is_a?(Exception) ErrorLog << "#{e.class}: #{e.message}\n" lines = $DEBUG ? e.backtrace : e.backtrace.find_all { |line| line !~ /bin\/macbacon|\/mac_bacon\.rb:\d+/ } lines.each_with_index { |line, i| ErrorLog << "\t#{line}#{i==0 ? ": #{@context.name} - #{@description}" : ""}\n" } ErrorLog << "\n" else # Pure NSException. ErrorLog << "#{e.name}: #{e.reason}\n" end @error = if e.kind_of? Error Counter[e.count_as] += 1 "#{e.count_as.to_s.upcase} - #{e}" else Counter[:errors] += 1 "ERROR: #{e.class} - #{e}" end end end end def self.add_context(context) (@contexts ||= []) << context end def self.current_context_index @current_context_index ||= 0 end def self.current_context @contexts[current_context_index] end def self.run @timer ||= Time.now Counter[:context_depth] += 1 handle_specification_begin(current_context.name) current_context.performSelector("run", withObject:nil, afterDelay:0) end def self.context_did_finish(context) handle_specification_end Counter[:context_depth] -= 1 if (@current_context_index + 1) < @contexts.size @current_context_index += 1 run else # DONE handle_summary exit(Counter.values_at(:failed, :errors).inject(:+)) end end class Context attr_reader :name, :block def initialize(name, before = nil, after = nil, &block) @name = name @before, @after = (before ? before.dup : []), (after ? after.dup : []) @block = block @specifications = [] @current_specification_index = 0 Bacon.add_context(self) instance_eval(&block) end def run # TODO #return unless name =~ RestrictContext if spec = current_specification spec.performSelector("run", withObject:nil, afterDelay:0) else Bacon.context_did_finish(self) end end def current_specification @specifications[@current_specification_index] end def specification_did_finish(spec) if (@current_specification_index + 1) < @specifications.size @current_specification_index += 1 run else Bacon.context_did_finish(self) end end def before(&block); @before << block; end def after(&block); @after << block; end def behaves_like(*names) names.each { |name| instance_eval(&Shared[name]) } end def it(description, &block) return unless description =~ RestrictName block ||= proc { should.flunk "not implemented" } Counter[:specifications] += 1 @specifications << Specification.new(self, description, block, @before, @after) end def should(*args, &block) if Counter[:depth]==0 it('should '+args.first,&block) else super(*args,&block) end end def describe(*args, &block) context = Bacon::Context.new(args.join(' '), @before, @after, &block) (parent_context = self).methods(false).each {|e| class< e e else false end def throw?(sym) catch(sym) { call return false } return true end def change? pre_result = yield called = call post_result = yield pre_result != post_result end end class Numeric def close?(to, delta) (to.to_f - self).abs <= delta.to_f rescue false end end class Object def should(*args, &block) Should.new(self).be(*args, &block) end end module Kernel private def describe(*args, &block) Bacon::Context.new(args.join(' '), &block) end def shared(name, &block) Bacon::Shared[name] = block end alias_method :context, :describe end class Should # Kills ==, ===, =~, eql?, equal?, frozen?, instance_of?, is_a?, # kind_of?, nil?, respond_to?, tainted? instance_methods.each { |name| undef_method name if name =~ /\?|^\W+$/ } def initialize(object) @object = object @negated = false end def not(*args, &block) @negated = !@negated if args.empty? self else be(*args, &block) end end def be(*args, &block) if args.empty? self else block = args.shift unless block_given? satisfy(*args, &block) end end alias a be alias an be def satisfy(*args, &block) if args.size == 1 && String === args.first description = args.shift else description = "" end r = yield(@object, *args) if Bacon::Counter[:depth] > 0 Bacon::Counter[:requirements] += 1 raise Bacon::Error.new(:failed, description) unless @negated ^ r r else @negated ? !r : !!r end end def method_missing(name, *args, &block) name = "#{name}?" if name.to_s =~ /\w[^?]\z/ desc = @negated ? "not " : "" desc << @object.inspect << "." << name.to_s desc << "(" << args.map{|x|x.inspect}.join(", ") << ") failed" satisfy(desc) { |x| x.__send__(name, *args, &block) } end def equal(value) self == value end def match(value) self =~ value end def identical_to(value) self.equal? value end alias same_as identical_to def flunk(reason="Flunked") raise Bacon::Error.new(:failed, reason) end end # Do not log all exceptions when running the specs. Exception.log_exceptions = false