Test Unit Sadism
Mutation count
Mutations that caused failures
Class being heckled
Name of class being heckled
Method being heckled
Name of method being heckled
Convenience methods
# File lib/heckle.rb, line 658 def aliasing_class(method_name) method_name.to_s =~ %rself\./ ? class << @klass; self; end : @klass end
# File lib/heckle.rb, line 677 def already_mutated? @mutated end
# File lib/heckle.rb, line 708 def current_code Ruby2Ruby.new.process current_tree end
# File lib/heckle.rb, line 516 def current_tree @current_tree.deep_clone end
# File lib/heckle.rb, line 605 def expand_dirs_to_files(dirs='.') Array(dirs).flatten.map { |p| if File.directory? p then Dir[File.join(p, '**', "*.rb")] else p end }.flatten end
# File lib/heckle.rb, line 588 def find_method sexp class_method = method_name.to_s =~ %r^self\./ clean_name = method_name.to_s.sub(%r^self\./, '').to_sym sexp = s(:block, sexp) unless sexp.first == :block sexp.each_sexp do |node| if class_method return node if node[0] == :defs && node[2] == clean_name else return node if node[0] == :defn && node[1] == clean_name end end nil end
# File lib/heckle.rb, line 560 def find_scope sexp, nesting=nil nesting ||= klass_name.split("::").map {|k| k.to_sym } current, *nesting = nesting sexp = s(:block, sexp) unless sexp.first == :block sexp.each_sexp do |node| next unless [:class, :module].include? node.first next unless node[1] == current block = node.detect {|s| Sexp === s && s[0] == :scope }[1] if nesting.empty? return sexp if method_name.nil? m = find_method block return m if m else s = find_scope block, nesting return s if s end end nil end
Copied from Flay#process
# File lib/heckle.rb, line 521 def find_scope_and_method expand_dirs_to_files.each do |file| #warn "Processing #{file}" if option[:verbose] ext = File.extname(file).sub(%r^\./, '') ext = "rb" if ext.nil? || ext.empty? msg = "process_#{ext}" unless respond_to? msg then warn " Unknown file type: #{ext}, defaulting to ruby" msg = "process_rb" end begin sexp = begin send msg, file rescue => e warn " #{e.message.strip}" warn " skipping #{file}" nil end next unless sexp found = find_scope sexp return found if found rescue SyntaxError => e warn " skipping #{file}: #{e.message}" end end raise "Couldn't find method." end
# File lib/heckle.rb, line 670 def grab_conditional_loop_parts(exp) cond = process(exp.shift) body = process(exp.shift) head_controlled = exp.shift return cond, body, head_controlled end
# File lib/heckle.rb, line 511 def grab_mutatees @walk_stack = [] walk_and_push current_tree end
# File lib/heckle.rb, line 218 def heckle(exp) @current_tree = exp.deep_clone src = begin Ruby2Ruby.new.process(exp) rescue => e puts "Error: #{e.message} with: #{klass_name}##{method_name}: #{@current_tree.inspect}" raise e end if @@debug original = Ruby2Ruby.new.process(@original_tree.deep_clone) @reporter.replacing(klass_name, method_name, original, src) end self.count += 1 clean_name = method_name.to_s.gsub(%rself\./, '') new_name = "h#{count}_#{clean_name}" klass = aliasing_class method_name klass.send :remove_method, new_name rescue nil klass.send :alias_method, new_name, clean_name klass.send :remove_method, clean_name rescue nil @klass.class_eval src, "(#{new_name})" end
# File lib/heckle.rb, line 647 def increment_mutation_count(node) # So we don't re-mutate this later if the tree is reset mutation_count[node] += 1 mutatee_type = @mutatees[node.first] mutatee_type.delete_at mutatee_type.index(node) @mutated = true end
# File lib/heckle.rb, line 643 def increment_node_count(node) node_count[node] += 1 end
# File lib/heckle.rb, line 315 def mutate_asgn(node) type = node.shift var = node.shift if node.empty? then s(type, :_heckle_dummy) else if node.last.first == :nil then s(type, var, s(:lit, 42)) else s(type, var, s(:nil)) end end end
Replaces the call node with nil.
# File lib/heckle.rb, line 258 def mutate_call(node) s(:nil) end
Replaces the value of the cvasgn with nil if its some value, and 42 if its nil.
Replaces the value of the dasgn with nil if its some value, and 42 if its nil.
Replaces the value of the dasgn_curr with nil if its some value, and 42 if its nil.
Swaps for a :true node.
# File lib/heckle.rb, line 449 def mutate_false(node) s(:true) end
Replaces the value of the gasgn with nil if its some value, and 42 if its nil.
Replaces the value of the iasgn with nil if its some value, and 42 if its nil.
Swaps the then and else parts of the :if node.
# File lib/heckle.rb, line 427 def mutate_if(node) s(:if, node[1], node[3], node[2]) end
# File lib/heckle.rb, line 302 def mutate_iter(exp) s(:nil) end
Replaces the value of the lasgn with nil if its some value, and 42 if its nil.
Replaces the value of the :lit node with a random value.
# File lib/heckle.rb, line 396 def mutate_lit(exp) case exp[1] when Fixnum, Float, Bignum s(:lit, exp[1] + rand_number) when Symbol s(:lit, rand_symbol) when Regexp s(:lit, Regexp.new(Regexp.escape(rand_string.gsub(%r\//, '\/')))) when Range s(:lit, rand_range) end end
# File lib/heckle.rb, line 477 def mutate_node(node) raise UnsupportedNodeError unless respond_to? "mutate_#{node.first}" increment_node_count node if should_heckle? node then increment_mutation_count node return send("mutate_#{node.first}", node) else node end end
Replaces the value of the :str node with a random value.
# File lib/heckle.rb, line 416 def mutate_str(node) s(:str, rand_string) end
Swaps for a :false node.
# File lib/heckle.rb, line 438 def mutate_true(node) s(:false) end
Swaps for a :while node.
# File lib/heckle.rb, line 473 def mutate_until(node) s(:while, node[1], node[2], node[3]) end
Swaps for a :until node.
# File lib/heckle.rb, line 461 def mutate_while(node) s(:until, node[1], node[2], node[3]) end
# File lib/heckle.rb, line 681 def mutations_left @last_mutations_left ||= -1 sum = 0 @mutatees.each { |mut| sum += mut.last.size } if sum == @last_mutations_left then puts 'bug!' puts require 'pp' puts 'mutatees:' pp @mutatees puts puts 'original tree:' pp @original_tree puts puts "Infinite loop detected!" puts "Please save this output to an attachment and submit a ticket here:" puts "http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921" exit 1 else @last_mutations_left = sum end sum end
# File lib/heckle.rb, line 306 def process_asgn(type, exp) var = exp.shift if exp.empty? then mutate_node s(type, var) else mutate_node s(type, var, process(exp.shift)) end end
Processing sexps
# File lib/heckle.rb, line 247 def process_call(exp) recv = process(exp.shift) meth = exp.shift args = process(exp.shift) mutate_node s(:call, recv, meth, args) end
# File lib/heckle.rb, line 329 def process_cvasgn(exp) process_asgn :cvasgn, exp end
# File lib/heckle.rb, line 339 def process_dasgn(exp) process_asgn :dasgn, exp end
# File lib/heckle.rb, line 349 def process_dasgn_curr(exp) process_asgn :dasgn_curr, exp end
# File lib/heckle.rb, line 262 def process_defn(exp) self.method = exp.shift result = s(:defn, method) result << process(exp.shift) until exp.empty? heckle(result) if method == method_name return result ensure @mutated = false node_count.clear end
# File lib/heckle.rb, line 274 def process_defs(exp) recv = process exp.shift meth = exp.shift self.method = "#{Ruby2Ruby.new.process(recv.deep_clone)}.#{meth}".intern result = s(:defs, recv, meth) result << process(exp.shift) until exp.empty? heckle(result) if method == method_name return result ensure @mutated = false node_count.clear end
# File lib/heckle.rb, line 442 def process_false(exp) mutate_node s(:false) end
# File lib/heckle.rb, line 369 def process_gasgn(exp) process_asgn :gasgn, exp end
# File lib/heckle.rb, line 359 def process_iasgn(exp) process_asgn :iasgn, exp end
# File lib/heckle.rb, line 420 def process_if(exp) mutate_node s(:if, process(exp.shift), process(exp.shift), process(exp.shift)) end
So #process_call works correctly
# File lib/heckle.rb, line 294 def process_iter(exp) call = process exp.shift args = process exp.shift body = process exp.shift mutate_node s(:iter, call, args, body) end
# File lib/heckle.rb, line 379 def process_lasgn(exp) process_asgn :lasgn, exp end
# File lib/heckle.rb, line 389 def process_lit(exp) mutate_node s(:lit, exp.shift) end
# File lib/heckle.rb, line 556 def process_rb file RubyParser.new.process(File.read(file), file) end
# File lib/heckle.rb, line 409 def process_str(exp) mutate_node s(:str, exp.shift) end
# File lib/heckle.rb, line 431 def process_true(exp) mutate_node s(:true) end
# File lib/heckle.rb, line 465 def process_until(exp) cond, body, head_controlled = grab_conditional_loop_parts(exp) mutate_node s(:until, cond, body, head_controlled) end
# File lib/heckle.rb, line 453 def process_while(exp) cond, body, head_controlled = grab_conditional_loop_parts(exp) mutate_node s(:while, cond, body, head_controlled) end
Returns a random Fixnum.
# File lib/heckle.rb, line 715 def rand_number (rand(100) + 1)*((-1)**rand(2)) end
Returns a random Range
# File lib/heckle.rb, line 742 def rand_range min = rand(50) max = min + rand(50) min..max end
Returns a random String
# File lib/heckle.rb, line 722 def rand_string size = rand(50) str = "" size.times { str << rand(126).chr } str end
Returns a random Symbol
# File lib/heckle.rb, line 732 def rand_symbol letters = ('a'..'z').to_a + ('A'..'Z').to_a str = "" (rand(50) + 1).times { str << letters[rand(letters.size)] } :"#{str}" end
# File lib/heckle.rb, line 214 def record_passing_mutation @failures << current_code end
# File lib/heckle.rb, line 615 def reset reset_tree reset_mutatees mutation_count.clear end
# File lib/heckle.rb, line 639 def reset_mutatees @mutatees = @original_mutatees.deep_clone end
# File lib/heckle.rb, line 621 def reset_tree return unless original_tree != current_tree @mutated = false @current_tree = original_tree.deep_clone self.count += 1 clean_name = method_name.to_s.gsub(%rself\./, '') new_name = "h#{count}_#{clean_name}" klass = aliasing_class method_name klass.send :undef_method, new_name rescue nil klass.send :alias_method, new_name, clean_name klass.send :alias_method, clean_name, "h1_#{clean_name}" end
# File lib/heckle.rb, line 160 def run_tests if tests_pass? then record_passing_mutation else @reporter.report_test_failures end end
# File lib/heckle.rb, line 662 def should_heckle?(exp) return false unless method == method_name return false if node_count[exp] <= mutation_count[exp] key = exp.first.to_sym mutatees.include?(key) && mutatees[key].include?(exp) && !already_mutated? end
Suppresses output on $stdout and $stderr.
# File lib/heckle.rb, line 751 def silence_stream return yield if @@debug begin dead = File.open("/dev/null", "w") $stdout.flush $stderr.flush oldstdout = $stdout.dup oldstderr = $stderr.dup $stdout.reopen(dead) $stderr.reopen(dead) result = yield ensure $stdout.flush $stderr.flush $stdout.reopen(oldstdout) $stderr.reopen(oldstderr) result end end
Overwrite test_pass? for your own Heckle runner.
# File lib/heckle.rb, line 156 def tests_pass? raise NotImplementedError end
Running the script
# File lib/heckle.rb, line 171 def validate left = mutations_left if left == 0 then @reporter.no_mutations(method_name) return end @reporter.method_loaded(klass_name, method_name, left) until left == 0 do @reporter.remaining_mutations left reset_tree begin process current_tree timeout(@@timeout, Heckle::Timeout) { run_tests } rescue SyntaxError => e @reporter.warning "Mutation caused a syntax error:\n\n#{e.message}}" rescue Heckle::Timeout @reporter.warning "Your tests timed out. Heckle may have caused an infinite loop." rescue Interrupt @reporter.warning 'Mutation canceled, hit ^C again to exit' sleep 2 end left = mutations_left end reset # in case we're validating again. we should clean up. unless @failures.empty? @reporter.no_failures @failures.each do |failure| original = Ruby2Ruby.new.process(@original_tree.deep_clone) @reporter.failure(original, failure) end false else @reporter.no_surviving_mutants true end end
Tree operations
# File lib/heckle.rb, line 492 def walk_and_push(node, index = 0) return unless node.respond_to? :each return if node.is_a? String @walk_stack.push node.first node.each_with_index { |child_node, i| walk_and_push child_node, i } @walk_stack.pop if @mutatable_nodes.include? node.first and # HACK skip over call nodes that are the first child of an iter or # they'll get added twice # # I think heckle really needs two processors, one for finding and one # for heckling. !(node.first == :call and index == 1 and @walk_stack.last == :iter) then @mutatees[node.first].push(node) end end
# File lib/heckle.rb, line 96 def self.debug @@debug end
# File lib/heckle.rb, line 100 def self.debug=(value) @@debug = value end
# File lib/heckle.rb, line 109 def self.guess_timeout? @@guess_timeout end
Creates a new Heckle that will heckle
klass_name
and method_name
, sending results to
reporter
.
# File lib/heckle.rb, line 117 def initialize(klass_name = nil, method_name = nil, nodes = Heckle::MUTATABLE_NODES, reporter = Reporter.new) super() @klass_name = klass_name @method_name = method_name.intern if method_name @klass = klass_name.to_class @method = nil @reporter = reporter self.strict = false self.auto_shift_type = true self.expected = Sexp @mutatees = Hash.new @mutation_count = Hash.new 0 @node_count = Hash.new 0 @count = 0 @mutatable_nodes = nodes @mutatable_nodes.each {|type| @mutatees[type] = [] } @failures = [] @mutated = false @original_tree = rewrite find_scope_and_method @current_tree = @original_tree.deep_clone grab_mutatees @original_mutatees = mutatees.deep_clone end
# File lib/heckle.rb, line 104 def self.timeout=(value) @@timeout = value @@guess_timeout = false # We've set the timeout, don't guess end