class Heckle

Test Unit Sadism

Constants

ASGN_NODES

All assignment nodes that can be mutated by Heckle..

BRANCH_NODES

Branch node types.

DIFF

diff(1) executable

MUTATABLE_NODES

All nodes that can be mutated by Heckle.

NULL_PATH

Path to the bit bucket.

VERSION

The version of Heckle you are using.

WINDOZE

Is this platform MS Windows-like?

Attributes

count[RW]

Mutation count

failures[RW]

Mutations that caused failures

klass[RW]

Class being heckled

klass_name[RW]

Name of class being heckled

method[RW]

Method being heckled

method_name[RW]

Name of method being heckled

Public Class Methods

debug() click to toggle source
# File lib/heckle.rb, line 85
def self.debug
  @@debug
end
debug=(value) click to toggle source
# File lib/heckle.rb, line 89
def self.debug=(value)
  @@debug = value
end
guess_timeout?() click to toggle source
# File lib/heckle.rb, line 98
def self.guess_timeout?
  @@guess_timeout
end
new(klass_name = nil, method_name = nil, nodes = Heckle::MUTATABLE_NODES, reporter = Reporter.new) click to toggle source

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
timeout=(value) click to toggle source
# File lib/heckle.rb, line 93
def self.timeout=(value)
  @@timeout = value
  @@guess_timeout = false # We've set the timeout, don't guess
end

Public Instance Methods

aliasing_class(method_name) click to toggle source

Convenience methods

# File lib/heckle.rb, line 552
def aliasing_class(method_name)
  method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
end
already_mutated?() click to toggle source
# File lib/heckle.rb, line 571
def already_mutated?
  @mutated
end
current_code() click to toggle source
# File lib/heckle.rb, line 602
def current_code
  Ruby2Ruby.translate(klass_name.to_class, method_name)
end
current_tree() click to toggle source
# 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
grab_conditional_loop_parts(exp) click to toggle source
# 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
grab_mutatees() click to toggle source
# File lib/heckle.rb, line 496
def grab_mutatees
  @walk_stack = []
  walk_and_push current_tree
end
heckle(exp) click to toggle source
# 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
increment_mutation_count(node) click to toggle source
# 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
increment_node_count(node) click to toggle source
# File lib/heckle.rb, line 537
def increment_node_count(node)
  node_count[node] += 1
end
mutate_asgn(node) click to toggle source
# 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
mutate_call(node) click to toggle source

Replaces the call node with nil.

# File lib/heckle.rb, line 243
def mutate_call(node)
  s(:nil)
end
mutate_cvasgn(node) click to toggle source

Replaces the value of the cvasgn with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_dasgn(node) click to toggle source

Replaces the value of the dasgn with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_dasgn_curr(node) click to toggle source

Replaces the value of the dasgn_curr with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_false(node) click to toggle source

Swaps for a :true node.

# File lib/heckle.rb, line 434
def mutate_false(node)
  s(:true)
end
mutate_gasgn(node) click to toggle source

Replaces the value of the gasgn with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_iasgn(node) click to toggle source

Replaces the value of the iasgn with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_if(node) click to toggle source

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
mutate_iter(exp) click to toggle source
# File lib/heckle.rb, line 287
def mutate_iter(exp)
  s(:nil)
end
mutate_lasgn(node) click to toggle source

Replaces the value of the lasgn with nil if its some value, and 42 if its nil.

Alias for: mutate_asgn
mutate_lit(exp) click to toggle source

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
mutate_node(node) click to toggle source
# 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
mutate_str(node) click to toggle source

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
mutate_true(node) click to toggle source

Swaps for a :false node.

# File lib/heckle.rb, line 423
def mutate_true(node)
  s(:false)
end
mutate_until(node) click to toggle source

Swaps for a :while node.

# File lib/heckle.rb, line 458
def mutate_until(node)
  s(:while, node[1], node[2], node[3])
end
mutate_while(node) click to toggle source

Swaps for a :until node.

# File lib/heckle.rb, line 446
def mutate_while(node)
  s(:until, node[1], node[2], node[3])
end
mutations_left() click to toggle source
# 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
process_asgn(type, exp) click to toggle source
# 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
process_call(exp) click to toggle source

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
process_cvasgn(exp) click to toggle source
# File lib/heckle.rb, line 314
def process_cvasgn(exp)
  process_asgn :cvasgn, exp
end
process_dasgn(exp) click to toggle source
# File lib/heckle.rb, line 324
def process_dasgn(exp)
  process_asgn :dasgn, exp
end
process_dasgn_curr(exp) click to toggle source
# File lib/heckle.rb, line 334
def process_dasgn_curr(exp)
  process_asgn :dasgn_curr, exp
end
process_defn(exp) click to toggle source
# 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
process_defs(exp) click to toggle source
# 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
process_false(exp) click to toggle source
# File lib/heckle.rb, line 427
def process_false(exp)
  mutate_node s(:false)
end
process_gasgn(exp) click to toggle source
# File lib/heckle.rb, line 354
def process_gasgn(exp)
  process_asgn :gasgn, exp
end
process_iasgn(exp) click to toggle source
# File lib/heckle.rb, line 344
def process_iasgn(exp)
  process_asgn :iasgn, exp
end
process_if(exp) click to toggle source
# File lib/heckle.rb, line 405
def process_if(exp)
  mutate_node s(:if, process(exp.shift), process(exp.shift), process(exp.shift))
end
process_iter(exp) click to toggle source

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
process_lasgn(exp) click to toggle source
# File lib/heckle.rb, line 364
def process_lasgn(exp)
  process_asgn :lasgn, exp
end
process_lit(exp) click to toggle source
# File lib/heckle.rb, line 374
def process_lit(exp)
  mutate_node s(:lit, exp.shift)
end
process_str(exp) click to toggle source
# File lib/heckle.rb, line 394
def process_str(exp)
  mutate_node s(:str, exp.shift)
end
process_true(exp) click to toggle source
# File lib/heckle.rb, line 416
def process_true(exp)
  mutate_node s(:true)
end
process_until(exp) click to toggle source
# 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
process_while(exp) click to toggle source
# 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
rand_number() click to toggle source

Returns a random Fixnum.

# File lib/heckle.rb, line 609
def rand_number
  (rand(100) + 1)*((-1)**rand(2))
end
rand_range() click to toggle source

Returns a random Range

# File lib/heckle.rb, line 636
def rand_range
  min = rand(50)
  max = min + rand(50)
  min..max
end
rand_string() click to toggle source

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
rand_symbol() click to toggle source

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
record_passing_mutation() click to toggle source
# File lib/heckle.rb, line 201
def record_passing_mutation
  @failures << current_code
end
reset() click to toggle source
# File lib/heckle.rb, line 511
def reset
  reset_tree
  reset_mutatees
  mutation_count.clear
end
reset_mutatees() click to toggle source
# File lib/heckle.rb, line 533
def reset_mutatees
  @mutatees = @original_mutatees.deep_clone
end
reset_tree() click to toggle source
# 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
run_tests() click to toggle source
# File lib/heckle.rb, line 147
def run_tests
  if tests_pass? then
    record_passing_mutation
  else
    @reporter.report_test_failures
  end
end
should_heckle?(exp) click to toggle source
# 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
silence_stream() { || ... } click to toggle source

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
tests_pass?() click to toggle source

Overwrite test_pass? for your own Heckle runner.

# File lib/heckle.rb, line 143
def tests_pass?
  raise NotImplementedError
end
validate() click to toggle source

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
walk_and_push(node, index = 0) click to toggle source

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