2012-07-23 10:37:44 -04:00
|
|
|
$LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
|
|
|
|
|
|
|
|
# original code by Ashley Moran:
|
|
|
|
# http://aviewfromafar.net/2007/11/1/rake-task-for-heckling-your-specs
|
|
|
|
|
|
|
|
begin
|
|
|
|
require 'pathname'
|
|
|
|
require 'backports'
|
|
|
|
require 'active_support/inflector'
|
|
|
|
require 'heckle'
|
|
|
|
require 'mspec'
|
|
|
|
require 'mspec/utils/name_map'
|
|
|
|
|
|
|
|
SKIP_METHODS = %w[ blank_slate_method_added ].freeze
|
|
|
|
|
|
|
|
class NameMap
|
|
|
|
def file_name(method, constant)
|
|
|
|
map = MAP[method]
|
|
|
|
name = if map
|
|
|
|
map[constant] || map[:default]
|
|
|
|
else
|
|
|
|
method.
|
|
|
|
gsub('?','_ques').
|
|
|
|
gsub('!','_bang').
|
|
|
|
gsub('=','_assign')
|
|
|
|
end
|
|
|
|
"#{name}_spec.rb"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
desc 'Heckle each module and class'
|
2012-07-23 19:41:08 -04:00
|
|
|
task :heckle do
|
2012-07-23 10:37:44 -04:00
|
|
|
unless Ruby2Ruby::VERSION == '1.2.2'
|
|
|
|
raise "ruby2ruby version #{Ruby2Ruby::VERSION} may not work properly, 1.2.2 *only* is recommended for use with heckle"
|
|
|
|
end
|
|
|
|
|
2012-07-26 13:25:23 -04:00
|
|
|
require File.expand_path('../../../spec/support/fake_ast',__FILE__)
|
2012-07-23 19:41:08 -04:00
|
|
|
require 'mutant'
|
2012-07-23 10:37:44 -04:00
|
|
|
|
|
|
|
root_module_regexp = Regexp.union('Mutant')
|
|
|
|
|
|
|
|
spec_dir = Pathname('spec/unit')
|
|
|
|
|
|
|
|
NameMap::MAP.each do |op, method|
|
|
|
|
next if method.kind_of?(Hash)
|
|
|
|
NameMap::MAP[op] = { :default => method }
|
|
|
|
end
|
|
|
|
|
|
|
|
aliases = Hash.new { |h,mod| h[mod] = Hash.new { |h,method| h[method] = method } }
|
|
|
|
map = NameMap.new
|
|
|
|
|
|
|
|
heckle_caught_modules = Hash.new { |hash, key| hash[key] = [] }
|
|
|
|
unhandled_mutations = 0
|
|
|
|
|
|
|
|
ObjectSpace.each_object(Module) do |mod|
|
|
|
|
next unless mod.name =~ /\A#{root_module_regexp}(?::|\z)/
|
|
|
|
|
2012-07-26 13:25:23 -04:00
|
|
|
# Mutation::Loader is to rbx specific
|
|
|
|
next if mod == Mutant::Loader
|
|
|
|
# Mutation::Matcher::Method is to rbx specific
|
|
|
|
next if mod == Mutant::Matcher::Method
|
|
|
|
# Mutation::Context::Constant is to rbx specific
|
|
|
|
next if mod == Mutant::Context::Constant
|
|
|
|
|
2012-07-23 10:37:44 -04:00
|
|
|
spec_prefix = spec_dir.join(mod.name.underscore)
|
|
|
|
|
|
|
|
specs = []
|
|
|
|
|
|
|
|
# get the public class methods
|
|
|
|
metaclass = class << mod; self end
|
|
|
|
ancestors = metaclass.ancestors
|
|
|
|
|
|
|
|
spec_class_methods = mod.singleton_methods(false)
|
|
|
|
|
|
|
|
spec_class_methods.reject! do |method|
|
|
|
|
%w[ yaml_new yaml_tag_subclasses? included nesting constants ].include?(method.to_s)
|
|
|
|
end
|
|
|
|
|
|
|
|
if mod.ancestors.include?(Singleton)
|
|
|
|
spec_class_methods.reject! { |method| method.to_s == 'instance' }
|
|
|
|
end
|
|
|
|
|
|
|
|
# get the protected and private class methods
|
|
|
|
other_class_methods = metaclass.protected_instance_methods(false) |
|
|
|
|
metaclass.private_instance_methods(false)
|
|
|
|
|
|
|
|
ancestors.each do |ancestor|
|
|
|
|
other_class_methods -= ancestor.protected_instance_methods(false) |
|
|
|
|
ancestor.private_instance_methods(false)
|
|
|
|
end
|
|
|
|
|
|
|
|
other_class_methods.reject! do |method|
|
|
|
|
method.to_s == 'allocate' || SKIP_METHODS.include?(method.to_s)
|
|
|
|
end
|
|
|
|
|
|
|
|
other_class_methods.reject! do |method|
|
|
|
|
next unless spec_class_methods.any? { |specced| specced.to_s == $1 }
|
|
|
|
|
|
|
|
spec_class_methods << method
|
|
|
|
end
|
|
|
|
|
|
|
|
# get the instances methods
|
|
|
|
spec_methods = mod.public_instance_methods(false)
|
|
|
|
|
|
|
|
other_methods = mod.protected_instance_methods(false) |
|
|
|
|
mod.private_instance_methods(false)
|
|
|
|
|
|
|
|
other_methods.reject! do |method|
|
|
|
|
next unless spec_methods.any? { |specced| specced.to_s == $1 }
|
|
|
|
|
|
|
|
spec_methods << method
|
|
|
|
end
|
|
|
|
|
|
|
|
# map the class methods to spec files
|
|
|
|
spec_class_methods.each do |method|
|
|
|
|
method = aliases[mod.name][method]
|
|
|
|
next if SKIP_METHODS.include?(method.to_s)
|
|
|
|
|
|
|
|
spec_file = spec_prefix.join('class_methods').join(map.file_name(method, mod.name))
|
|
|
|
|
|
|
|
unless spec_file.file?
|
|
|
|
raise "No spec file #{spec_file} for #{mod}.#{method}"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
specs << [ ".#{method}", [ spec_file ] ]
|
|
|
|
end
|
|
|
|
|
|
|
|
# map the instance methods to spec files
|
|
|
|
spec_methods.each do |method|
|
|
|
|
method = aliases[mod.name][method]
|
|
|
|
next if SKIP_METHODS.include?(method.to_s)
|
|
|
|
|
|
|
|
spec_file = spec_prefix.join(map.file_name(method, mod.name))
|
|
|
|
|
|
|
|
unless spec_file.file?
|
|
|
|
raise "No spec file #{spec_file} for #{mod}##{method}"
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
specs << [ "##{method}", [ spec_file ] ]
|
|
|
|
end
|
|
|
|
|
|
|
|
# non-public methods are considered covered if they can be mutated
|
|
|
|
# and any spec fails for the current or descendant modules
|
|
|
|
other_methods.each do |method|
|
|
|
|
descedant_specs = []
|
|
|
|
|
|
|
|
ObjectSpace.each_object(Module) do |descedant|
|
|
|
|
next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
|
|
|
|
descedant_spec_prefix = spec_dir.join(descedant.name.underscore)
|
|
|
|
descedant_specs << descedant_spec_prefix
|
|
|
|
|
|
|
|
if method.to_s == 'initialize'
|
|
|
|
descedant_specs.concat(Pathname.glob(descedant_spec_prefix.join('class_methods/new_spec.rb')))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
specs << [ "##{method}", descedant_specs ]
|
|
|
|
end
|
|
|
|
|
|
|
|
other_class_methods.each do |method|
|
|
|
|
descedant_specs = []
|
|
|
|
|
|
|
|
ObjectSpace.each_object(Module) do |descedant|
|
|
|
|
next unless descedant.name =~ /\A#{root_module_regexp}(?::|\z)/ && mod >= descedant
|
|
|
|
descedant_specs << spec_dir.join(descedant.name.underscore).join('class_methods')
|
|
|
|
end
|
|
|
|
|
|
|
|
specs << [ ".#{method}", descedant_specs ]
|
|
|
|
end
|
|
|
|
|
|
|
|
specs.sort.each do |(method, spec_files)|
|
|
|
|
puts "Heckling #{mod}#{method}"
|
|
|
|
IO.popen("spec #{spec_files.join(' ')} --heckle '#{mod}#{method}'") do |pipe|
|
|
|
|
while line = pipe.gets
|
|
|
|
case line = line.chomp
|
|
|
|
when "The following mutations didn't cause test failures:"
|
|
|
|
heckle_caught_modules[mod.name] << method
|
|
|
|
when '+++ mutation'
|
|
|
|
unhandled_mutations += 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if unhandled_mutations > 0
|
|
|
|
error_message_lines = [ "*************\n" ]
|
|
|
|
|
|
|
|
error_message_lines << "Heckle found #{unhandled_mutations} " \
|
|
|
|
"mutation#{"s" unless unhandled_mutations == 1} " \
|
|
|
|
"that didn't cause spec violations\n"
|
|
|
|
|
|
|
|
heckle_caught_modules.each do |mod, methods|
|
|
|
|
error_message_lines << "#{mod} contains the following " \
|
|
|
|
'poorly-specified methods:'
|
|
|
|
methods.each do |method|
|
|
|
|
error_message_lines << " - #{method}"
|
|
|
|
end
|
|
|
|
error_message_lines << ''
|
|
|
|
end
|
|
|
|
|
|
|
|
error_message_lines << 'Get your act together and come back ' \
|
|
|
|
'when your specs are doing their job!'
|
|
|
|
|
|
|
|
raise error_message_lines.join("\n")
|
|
|
|
else
|
|
|
|
puts 'Well done! Your code withstood a heckling.'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue LoadError
|
|
|
|
task :heckle => :spec do
|
|
|
|
$stderr.puts 'Heckle or mspec is not available. In order to run heckle, you must: gem install heckle mspec'
|
|
|
|
end
|
|
|
|
end
|