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 Instance Methods

aliasing_class(method_name) click to toggle source

Convenience methods

# File lib/heckle.rb, line 658
def aliasing_class(method_name)
  method_name.to_s =~ %rself\./ ? class << @klass; self; end : @klass
end
already_mutated?() click to toggle source
# File lib/heckle.rb, line 677
def already_mutated?
  @mutated
end
current_code() click to toggle source
# File lib/heckle.rb, line 708
def current_code
  Ruby2Ruby.new.process current_tree
end
current_tree() click to toggle source
# File lib/heckle.rb, line 516
def current_tree
  @current_tree.deep_clone
end
expand_dirs_to_files(dirs='.') click to toggle source
# 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
find_method(sexp) click to toggle source
# 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
find_scope(sexp, nesting=nil) click to toggle source
# 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
find_scope_and_method() click to toggle source

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
grab_conditional_loop_parts(exp) click to toggle source
# 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
grab_mutatees() click to toggle source
# File lib/heckle.rb, line 511
def grab_mutatees
  @walk_stack = []
  walk_and_push current_tree
end
heckle(exp) click to toggle source
# 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
increment_mutation_count(node) click to toggle source
# 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
increment_node_count(node) click to toggle source
# File lib/heckle.rb, line 643
def increment_node_count(node)
  node_count[node] += 1
end
mutate_asgn(node) click to toggle source
# 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
mutate_call(node) click to toggle source

Replaces the call node with nil.

# File lib/heckle.rb, line 258
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 449
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 427
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 302
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 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
mutate_node(node) click to toggle source
# 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
mutate_str(node) click to toggle source

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

Swaps for a :false node.

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

Swaps for a :while node.

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

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
process_cvasgn(exp) click to toggle source
# File lib/heckle.rb, line 329
def process_cvasgn(exp)
  process_asgn :cvasgn, exp
end
process_dasgn(exp) click to toggle source
# File lib/heckle.rb, line 339
def process_dasgn(exp)
  process_asgn :dasgn, exp
end
process_dasgn_curr(exp) click to toggle source
# File lib/heckle.rb, line 349
def process_dasgn_curr(exp)
  process_asgn :dasgn_curr, exp
end
process_defn(exp) click to toggle source
# 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
process_defs(exp) click to toggle source
# 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
process_false(exp) click to toggle source
# File lib/heckle.rb, line 442
def process_false(exp)
  mutate_node s(:false)
end
process_gasgn(exp) click to toggle source
# File lib/heckle.rb, line 369
def process_gasgn(exp)
  process_asgn :gasgn, exp
end
process_iasgn(exp) click to toggle source
# File lib/heckle.rb, line 359
def process_iasgn(exp)
  process_asgn :iasgn, exp
end
process_if(exp) click to toggle source
# File lib/heckle.rb, line 420
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 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
process_lasgn(exp) click to toggle source
# File lib/heckle.rb, line 379
def process_lasgn(exp)
  process_asgn :lasgn, exp
end
process_lit(exp) click to toggle source
# File lib/heckle.rb, line 389
def process_lit(exp)
  mutate_node s(:lit, exp.shift)
end
process_rb(file) click to toggle source
# File lib/heckle.rb, line 556
def process_rb file
  RubyParser.new.process(File.read(file), file)
end
process_str(exp) click to toggle source
# File lib/heckle.rb, line 409
def process_str(exp)
  mutate_node s(:str, exp.shift)
end
process_true(exp) click to toggle source
# File lib/heckle.rb, line 431
def process_true(exp)
  mutate_node s(:true)
end
process_until(exp) click to toggle source
# 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
process_while(exp) click to toggle source
# 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
rand_number() click to toggle source

Returns a random Fixnum.

# File lib/heckle.rb, line 715
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 742
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 722
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 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
record_passing_mutation() click to toggle source
# File lib/heckle.rb, line 214
def record_passing_mutation
  @failures << current_code
end
reset() click to toggle source
# File lib/heckle.rb, line 615
def reset
  reset_tree
  reset_mutatees
  mutation_count.clear
end
reset_mutatees() click to toggle source
# File lib/heckle.rb, line 639
def reset_mutatees
  @mutatees = @original_mutatees.deep_clone
end
reset_tree() click to toggle source
# 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
run_tests() click to toggle source
# File lib/heckle.rb, line 160
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 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
silence_stream() { || ... } click to toggle source

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

Overwrite test_pass? for your own Heckle runner.

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

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

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

Public Class Methods

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