mirror of
https://github.com/zhigang1992/RubyMotion.git
synced 2026-04-01 09:31:20 +08:00
729 lines
19 KiB
Ruby
729 lines
19 KiB
Ruby
# encoding: utf-8
|
|
|
|
# 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 <purl.org/net/chneukirchen>
|
|
#
|
|
# 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,
|
|
}
|
|
|
|
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
|
|
unless respond_to?(:handle_specification_begin)
|
|
extend(Outputs[ENV['output']] || SpecDoxOutput)
|
|
end
|
|
|
|
@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<<context; self end.send(:define_method, e) {|*args| parent_context.send(e, *args)}
|
|
}
|
|
context
|
|
end
|
|
|
|
def wait(seconds = nil, &block)
|
|
if seconds
|
|
current_specification.schedule_block(seconds, &block)
|
|
else
|
|
current_specification.postpone_block(&block)
|
|
end
|
|
end
|
|
|
|
def wait_max(timeout, &block)
|
|
current_specification.postpone_block(timeout, &block)
|
|
end
|
|
|
|
def wait_for_change(object_to_observe, key_path, timeout = 1, &block)
|
|
current_specification.postpone_block_until_change(object_to_observe, key_path, timeout, &block)
|
|
end
|
|
|
|
def resume
|
|
current_specification.resume
|
|
end
|
|
|
|
def raise?(*args, &block); block.raise?(*args); end
|
|
def throw?(*args, &block); block.throw?(*args); end
|
|
def change?(*args, &block); block.change?(*args); end
|
|
|
|
alias_method :context, :describe
|
|
end
|
|
end
|
|
|
|
|
|
class Object
|
|
def true?; false; end
|
|
def false?; false; end
|
|
end
|
|
|
|
class TrueClass
|
|
def true?; true; end
|
|
end
|
|
|
|
class FalseClass
|
|
def false?; true; end
|
|
end
|
|
|
|
class Proc
|
|
def raise?(*exceptions)
|
|
call
|
|
rescue *(exceptions.empty? ? RuntimeError : exceptions) => 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
|