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
# File lib/heckle.rb, line 85 def self.debug @@debug end
# File lib/heckle.rb, line 89 def self.debug=(value) @@debug = value end
# File lib/heckle.rb, line 98 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 106 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 grab_mutatees @original_tree = current_tree.deep_clone @original_mutatees = mutatees.deep_clone end
# File lib/heckle.rb, line 93 def self.timeout=(value) @@timeout = value @@guess_timeout = false # We've set the timeout, don't guess end
Convenience methods
# File lib/heckle.rb, line 552 def aliasing_class(method_name) method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass end
# File lib/heckle.rb, line 571 def already_mutated? @mutated end
# File lib/heckle.rb, line 602 def current_code Ruby2Ruby.translate(klass_name.to_class, method_name) end
# File lib/heckle.rb, line 501 def current_tree ur = Unifier.new sexp = ParseTree.translate(klass_name.to_class, method_name) raise "sexp invalid for #{klass_name}##{method_name}" if sexp == [nil] sexp = ur.process(sexp) rewrite sexp end
# File lib/heckle.rb, line 564 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 496 def grab_mutatees @walk_stack = [] walk_and_push current_tree end
# File lib/heckle.rb, line 205 def heckle(exp) exp_copy = exp.deep_clone src = begin Ruby2Ruby.new.process(exp) rescue => e puts "Error: #{e.message} with: #{klass_name}##{method_name}: #{exp_copy.inspect}" raise e end original = Ruby2Ruby.new.process(@original_tree.deep_clone) @reporter.replacing(klass_name, method_name, original, src) if @@debug clean_name = method_name.to_s.gsub(/self\./, '') self.count += 1 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 541 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 537 def increment_node_count(node) node_count[node] += 1 end
# File lib/heckle.rb, line 300 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 243 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 434 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 412 def mutate_if(node) s(:if, node[1], node[3], node[2]) end
# File lib/heckle.rb, line 287 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 381 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(/\//, '\/')))) when Range s(:lit, rand_range) end end
# File lib/heckle.rb, line 462 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 401 def mutate_str(node) s(:str, rand_string) end
Swaps for a :false node.
# File lib/heckle.rb, line 423 def mutate_true(node) s(:false) end
Swaps for a :while node.
# File lib/heckle.rb, line 458 def mutate_until(node) s(:while, node[1], node[2], node[3]) end
Swaps for a :until node.
# File lib/heckle.rb, line 446 def mutate_while(node) s(:until, node[1], node[2], node[3]) end
# File lib/heckle.rb, line 575 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 291 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 232 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 314 def process_cvasgn(exp) process_asgn :cvasgn, exp end
# File lib/heckle.rb, line 324 def process_dasgn(exp) process_asgn :dasgn, exp end
# File lib/heckle.rb, line 334 def process_dasgn_curr(exp) process_asgn :dasgn_curr, exp end
# File lib/heckle.rb, line 247 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 259 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 427 def process_false(exp) mutate_node s(:false) end
# File lib/heckle.rb, line 354 def process_gasgn(exp) process_asgn :gasgn, exp end
# File lib/heckle.rb, line 344 def process_iasgn(exp) process_asgn :iasgn, exp end
# File lib/heckle.rb, line 405 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 279 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 364 def process_lasgn(exp) process_asgn :lasgn, exp end
# File lib/heckle.rb, line 374 def process_lit(exp) mutate_node s(:lit, exp.shift) end
# File lib/heckle.rb, line 394 def process_str(exp) mutate_node s(:str, exp.shift) end
# File lib/heckle.rb, line 416 def process_true(exp) mutate_node s(:true) end
# File lib/heckle.rb, line 450 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 438 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 609 def rand_number (rand(100) + 1)*((-1)**rand(2)) end
Returns a random Range
# File lib/heckle.rb, line 636 def rand_range min = rand(50) max = min + rand(50) min..max end
Returns a random String
# File lib/heckle.rb, line 616 def rand_string size = rand(50) str = "" size.times { str << rand(126).chr } str end
Returns a random Symbol
# File lib/heckle.rb, line 626 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 201 def record_passing_mutation @failures << current_code end
# File lib/heckle.rb, line 511 def reset reset_tree reset_mutatees mutation_count.clear end
# File lib/heckle.rb, line 533 def reset_mutatees @mutatees = @original_mutatees.deep_clone end
# File lib/heckle.rb, line 517 def reset_tree return unless original_tree != current_tree @mutated = false self.count += 1 clean_name = method_name.to_s.gsub(/self\./, '') 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 147 def run_tests if tests_pass? then record_passing_mutation else @reporter.report_test_failures end end
# File lib/heckle.rb, line 556 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 645 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 143 def tests_pass? raise NotImplementedError end
Running the script
# File lib/heckle.rb, line 158 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 477 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