From d47548ad249ab4b910b3ff9fea986cd1fdb2a1f8 Mon Sep 17 00:00:00 2001 From: Laurent Sansonetti Date: Fri, 23 Dec 2011 22:39:47 +0100 Subject: [PATCH] add basic bacon/spec integration --- lib/motion/project.rb | 7 + lib/motion/project/builder.rb | 117 +++++--- lib/motion/project/config.rb | 15 +- lib/motion/spec.rb | 550 ++++++++++++++++++++++++++++++++++ 4 files changed, 645 insertions(+), 44 deletions(-) create mode 100644 lib/motion/spec.rb diff --git a/lib/motion/project.rb b/lib/motion/project.rb index 5a19bee0..8aa60123 100644 --- a/lib/motion/project.rb +++ b/lib/motion/project.rb @@ -71,6 +71,13 @@ task :archive => ['build:ios'] do end end +desc "Run specs" +task :spec do + App.config.name += '_spec' + App.config.spec_mode = true + Rake::Task["simulator"].invoke +end + desc "Deploy on the device" task :deploy => :archive do deploy = File.join(App.config.bindir, 'deploy') diff --git a/lib/motion/project/builder.rb b/lib/motion/project/builder.rb index 3c5f2a38..389f26d4 100644 --- a/lib/motion/project/builder.rb +++ b/lib/motion/project/builder.rb @@ -38,62 +38,68 @@ module Motion; module Project; end # Build object files. - objs = [] objs_build_dir = File.join(build_dir, config.sdk_version + '-sdk-objs') FileUtils.mkdir_p(objs_build_dir) project_file_changed = File.mtime(config.project_file) > File.mtime(objs_build_dir) - config.ordered_build_files.each do |path| - obj = File.join(objs_build_dir, "#{path}.o") + build_file = Proc.new do |path| + obj ||= File.join(objs_build_dir, "#{path}.o") should_rebuild = (project_file_changed \ or !File.exist?(obj) \ or File.mtime(path) > File.mtime(obj) \ or File.mtime(ruby) > File.mtime(obj)) # Generate or retrieve init function. - init_func = should_rebuild ? "MREP_#{`uuidgen`.strip.gsub('-', '')}" : `nm #{obj}`.scan(/T\s+_(MREP_.*)/)[0][0] - objs << [obj, init_func] + init_func = should_rebuild ? "MREP_#{`/usr/bin/uuidgen`.strip.gsub('-', '')}" : `/usr/bin/nm #{obj}`.scan(/T\s+_(MREP_.*)/)[0][0] - next unless should_rebuild - - arch_objs = [] - archs.each do |arch| - # Locate arch kernel. - kernel = File.join(datadir, platform, "kernel-#{arch}.bc") - raise "Can't locate kernel file" unless File.exist?(kernel) - - # Prepare build_dir. - bc = File.join(objs_build_dir, "#{path}.#{arch}.bc") - FileUtils.mkdir_p(File.dirname(bc)) - - # LLVM bitcode. - bs_flags = bs_files.map { |x| "--uses-bs \"" + x + "\" " }.join(' ') - sh "/usr/bin/env VM_KERNEL_PATH=\"#{kernel}\" #{ruby} #{bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"" - - # Assembly. - asm = File.join(objs_build_dir, "#{path}.#{arch}.s") - llc_arch = case arch - when 'i386'; 'x86' - when 'x86_64'; 'x86-64' - when /^arm/; 'arm' - else; arch + if should_rebuild + FileUtils.mkdir_p(File.dirname(obj)) + arch_objs = [] + archs.each do |arch| + # Locate arch kernel. + kernel = File.join(datadir, platform, "kernel-#{arch}.bc") + raise "Can't locate kernel file" unless File.exist?(kernel) + + # LLVM bitcode. + bc = File.join(objs_build_dir, "#{path}.#{arch}.bc") + bs_flags = bs_files.map { |x| "--uses-bs \"" + x + "\" " }.join(' ') + sh "/usr/bin/env VM_KERNEL_PATH=\"#{kernel}\" #{ruby} #{bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"" + + # Assembly. + asm = File.join(objs_build_dir, "#{path}.#{arch}.s") + llc_arch = case arch + when 'i386'; 'x86' + when 'x86_64'; 'x86-64' + when /^arm/; 'arm' + else; arch + end + sh "#{llc} \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch} -relocation-model=pic -disable-fp-elim -jit-enable-eh -disable-cfi" + + # Object. + arch_obj = File.join(objs_build_dir, "#{path}.#{arch}.o") + sh "#{cc} -fexceptions -c -arch #{arch} \"#{asm}\" -o \"#{arch_obj}\"" + + arch_objs << arch_obj end - sh "#{llc} \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch} -relocation-model=pic -disable-fp-elim -jit-enable-eh -disable-cfi" - - # Object. - arch_obj = File.join(objs_build_dir, "#{path}.#{arch}.o") - sh "#{cc} -fexceptions -c -arch #{arch} \"#{asm}\" -o \"#{arch_obj}\"" - - arch_objs << arch_obj + + # Assemble fat binary. + arch_objs_list = arch_objs.map { |x| "\"#{x}\"" }.join(' ') + sh "lipo -create #{arch_objs_list} -output \"#{obj}\"" end - - # Assemble fat binary. - arch_objs_list = arch_objs.map { |x| "\"#{x}\"" }.join(' ') - sh "lipo -create #{arch_objs_list} -output \"#{obj}\"" + + [obj, init_func] + end + objs = app_objs = config.ordered_build_files.map { |path| build_file.call(path) } + if config.spec_mode + # Build spec files too. + objs << build_file.call(File.expand_path(File.join(File.dirname(__FILE__), '../spec.rb'))) + spec_objs = config.spec_files.map { |path| build_file.call(path) } + objs += spec_objs end # Generate main file. main_txt = < + extern "C" { void ruby_sysinit(int *, char ***); void ruby_init(void); @@ -112,6 +118,36 @@ EOS end main_txt << < +# +# 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 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 TapOutput + def handle_specification_begin(name); end + def handle_specification_end ; end + + def handle_requirement_begin(description) + ErrorLog.replace "" + end + + def handle_requirement_end(error) + if error.empty? + puts "ok %-3d - %s" % [Counter[:specifications], description] + else + puts "not ok %d - %s: %s" % + [Counter[:specifications], description, error] + puts ErrorLog.strip.gsub(/^/, '# ') if Backtraces + end + 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) + 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 + + extend SpecDoxOutput # default + + 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 + + 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" + + @error = if e.kind_of? Error + Counter[e.count_as] += 1 + e.count_as.to_s.upcase + else + Counter[:errors] += 1 + "ERROR: #{e.class}" + 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 ||= lambda { 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 +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