lib/motion-spec/specification.rb
# -*- encoding : utf-8 -*-
module MotionSpec
class Specification
MULTIPLE_POSTPONES_ERROR_MESSAGE =
"Only one indefinite `wait' block at the same time is allowed!"
attr_reader :description
def initialize(context, description, block, before_filters, after_filters)
@context = context
@description = description
@block = block
@before_filters = before_filters.dup
@after_filters = 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) } }
Mocks.clear!
Stubs.clear!
end
def run
MotionSpec.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
return if @exception_occurred
@postponed_blocks_count += 1
if Platform.android?
sleep seconds
run_postponed_block(block)
else
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
return if @exception_occurred
fail MULTIPLE_POSTPONES_ERROR_MESSAGE if @postponed_block
@postponed_blocks_count += 1
@postponed_block = block
return performSelector(
'postponed_block_timeout_exceeded',
withObject: nil,
afterDelay: timeout
) unless Platform.android?
sleep timeout
postponed_block_timeout_exceeded
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
return if @exception_occurred
fail MULTIPLE_POSTPONES_ERROR_MESSAGE if @postponed_block
@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)
return performSelector(
'postponed_change_block_timeout_exceeded',
withObject: nil,
afterDelay: timeout
) unless Platform.android?
sleep timeout
postponed_change_block_timeout_exceeded
end
# rubocop:disable Lint/UnusedMethodArgument
def observeValueForKeyPath(_key_path, ofObject:object, change:_, context:__)
resume
end
# rubocop:enable Lint/UnusedMethodArgument
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 { fail Error.new(:failed, "timeout exceeded: #{@context.name} - #{@description}") }
@postponed_blocks_count = 0
finish_spec
end
def resume
unless Platform.android?
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: 'postponed_block_timeout_exceeded', object: nil)
NSObject.cancelPreviousPerformRequestsWithTarget(self, selector: 'postponed_change_block_timeout_exceeded', object: nil)
end
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 { fail Error.new(:missing, "empty specification: #{@context.name} #{@description}") }
end
run_after_filters
exit_spec unless postponed?
end
def cancel_scheduled_requests!
unless Platform.android?
NSObject.cancelPreviousPerformRequestsWithTarget(@context)
NSObject.cancelPreviousPerformRequestsWithTarget(self)
end
end
def exit_spec
cancel_scheduled_requests!
Counter[:depth] -= 1
MotionSpec.handle_requirement_end(@error)
@context.specification_did_finish(self)
end
def execute_block
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 do |line, i|
ErrorLog << "\t#{line}#{i == 0 ? ": #{@context.name} - #{@description}" : ''}\n"
end
ErrorLog << "\n"
else
if defined?(NSException)
# Pure NSException.
ErrorLog << "#{e.name}: #{e.reason}\n"
else
# Pure Java exception.
ErrorLog << "#{e.class.toString} : #{e.getMessage}"
end
end
@error =
if e.is_a? 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