Class: Heckle

Inherits:
SexpProcessor
  • Object
show all
Defined in:
lib/heckle.rb

Overview

Test Unit Sadism

Direct Known Subclasses

TestUnitHeckler

Defined Under Namespace

Classes: Reporter, Timeout

Constant Summary collapse

VERSION =

The version of Heckle you are using.

'1.4.3'
BRANCH_NODES =

Branch node types.

[:if, :until, :while]
WINDOZE =

Is this platform MS Windows-like?

RUBY_PLATFORM =~ /mswin/
NULL_PATH =

Path to the bit bucket.

WINDOZE ? 'NUL:' : '/dev/null'
DIFF =

diff(1) executable

WINDOZE ? 'diff.exe' : 'diff'
MUTATABLE_NODES =

All nodes that can be mutated by Heckle.

instance_methods.grep(/mutate_/).sort.map do |meth|
  meth.sub(/mutate_/, '').intern
end - [:asgn, :node]
ASGN_NODES =

All assignment nodes that can be mutated by Heckle..

MUTATABLE_NODES.map { |n| n.to_s }.grep(/asgn/).map do |n|
  n.intern
end
@@debug =
false
@@guess_timeout =
true
@@timeout =

default to something longer (can be overridden by runners)

60

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass_name = nil, method_name = nil, nodes = Heckle::MUTATABLE_NODES, reporter = Reporter.new) ⇒ Heckle

Creates a new Heckle that will heckle klass_name and method_name, sending results to reporter.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# 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

Instance Attribute Details

#countObject

Mutation count



49
50
51
# File 'lib/heckle.rb', line 49

def count
  @count
end

#failuresObject

Mutations that caused failures



54
55
56
# File 'lib/heckle.rb', line 54

def failures
  @failures
end

#klassObject

Class being heckled



59
60
61
# File 'lib/heckle.rb', line 59

def klass
  @klass
end

#klass_nameObject

Name of class being heckled



64
65
66
# File 'lib/heckle.rb', line 64

def klass_name
  @klass_name
end

#methodObject

Method being heckled



69
70
71
# File 'lib/heckle.rb', line 69

def method
  @method
end

#method_nameObject

Name of method being heckled



74
75
76
# File 'lib/heckle.rb', line 74

def method_name
  @method_name
end

#mutateesObject

:nodoc:



76
77
78
# File 'lib/heckle.rb', line 76

def mutatees
  @mutatees
end

#mutation_countObject

:nodoc:



77
78
79
# File 'lib/heckle.rb', line 77

def mutation_count
  @mutation_count
end

#node_countObject

:nodoc:



78
79
80
# File 'lib/heckle.rb', line 78

def node_count
  @node_count
end

#original_treeObject

:nodoc:



79
80
81
# File 'lib/heckle.rb', line 79

def original_tree
  @original_tree
end

Class Method Details

.debugObject



85
86
87
# File 'lib/heckle.rb', line 85

def self.debug
  @@debug
end

.debug=(value) ⇒ Object



89
90
91
# File 'lib/heckle.rb', line 89

def self.debug=(value)
  @@debug = value
end

.guess_timeout?Boolean

Returns:

  • (Boolean)


98
99
100
# File 'lib/heckle.rb', line 98

def self.guess_timeout?
  @@guess_timeout
end

.timeout=(value) ⇒ Object



93
94
95
96
# File 'lib/heckle.rb', line 93

def self.timeout=(value)
  @@timeout = value
  @@guess_timeout = false # We've set the timeout, don't guess
end

Instance Method Details

#aliasing_class(method_name) ⇒ Object

Convenience methods



552
553
554
# File 'lib/heckle.rb', line 552

def aliasing_class(method_name)
  method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
end

#already_mutated?Boolean

Returns:

  • (Boolean)


571
572
573
# File 'lib/heckle.rb', line 571

def already_mutated?
  @mutated
end

#current_codeObject



602
603
604
# File 'lib/heckle.rb', line 602

def current_code
  Ruby2Ruby.translate(klass_name.to_class, method_name)
end

#current_treeObject



501
502
503
504
505
506
507
508
509
# 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) ⇒ Object



564
565
566
567
568
569
# 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_mutateesObject



496
497
498
499
# File 'lib/heckle.rb', line 496

def grab_mutatees
  @walk_stack = []
  walk_and_push current_tree
end

#heckle(exp) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# 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) ⇒ Object



541
542
543
544
545
546
547
# 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) ⇒ Object



537
538
539
# File 'lib/heckle.rb', line 537

def increment_node_count(node)
  node_count[node] += 1
end

#mutate_asgn(node) ⇒ Object Also known as: mutate_cvasgn, mutate_dasgn, mutate_dasgn_curr, mutate_iasgn, mutate_gasgn, mutate_lasgn



300
301
302
303
304
305
306
307
308
309
310
311
312
# 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) ⇒ Object

Replaces the call node with nil.



243
244
245
# File 'lib/heckle.rb', line 243

def mutate_call(node)
  s(:nil)
end

#mutate_false(node) ⇒ Object

Swaps for a :true node.



434
435
436
# File 'lib/heckle.rb', line 434

def mutate_false(node)
  s(:true)
end

#mutate_if(node) ⇒ Object

Swaps the then and else parts of the :if node.



412
413
414
# File 'lib/heckle.rb', line 412

def mutate_if(node)
  s(:if, node[1], node[3], node[2])
end

#mutate_iter(exp) ⇒ Object



287
288
289
# File 'lib/heckle.rb', line 287

def mutate_iter(exp)
  s(:nil)
end

#mutate_lit(exp) ⇒ Object

Replaces the value of the :lit node with a random value.



381
382
383
384
385
386
387
388
389
390
391
392
# 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) ⇒ Object

Raises:

  • (UnsupportedNodeError)


462
463
464
465
466
467
468
469
470
471
472
# 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) ⇒ Object

Replaces the value of the :str node with a random value.



401
402
403
# File 'lib/heckle.rb', line 401

def mutate_str(node)
  s(:str, rand_string)
end

#mutate_true(node) ⇒ Object

Swaps for a :false node.



423
424
425
# File 'lib/heckle.rb', line 423

def mutate_true(node)
  s(:false)
end

#mutate_until(node) ⇒ Object

Swaps for a :while node.



458
459
460
# File 'lib/heckle.rb', line 458

def mutate_until(node)
  s(:while, node[1], node[2], node[3])
end

#mutate_while(node) ⇒ Object

Swaps for a :until node.



446
447
448
# File 'lib/heckle.rb', line 446

def mutate_while(node)
  s(:until, node[1], node[2], node[3])
end

#mutations_leftObject



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# 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) ⇒ Object



291
292
293
294
295
296
297
298
# 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) ⇒ Object

Processing sexps



232
233
234
235
236
237
238
# 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) ⇒ Object



314
315
316
# File 'lib/heckle.rb', line 314

def process_cvasgn(exp)
  process_asgn :cvasgn, exp
end

#process_dasgn(exp) ⇒ Object



324
325
326
# File 'lib/heckle.rb', line 324

def process_dasgn(exp)
  process_asgn :dasgn, exp
end

#process_dasgn_curr(exp) ⇒ Object



334
335
336
# File 'lib/heckle.rb', line 334

def process_dasgn_curr(exp)
  process_asgn :dasgn_curr, exp
end

#process_defn(exp) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
# 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) ⇒ Object



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# 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) ⇒ Object



427
428
429
# File 'lib/heckle.rb', line 427

def process_false(exp)
  mutate_node s(:false)
end

#process_gasgn(exp) ⇒ Object



354
355
356
# File 'lib/heckle.rb', line 354

def process_gasgn(exp)
  process_asgn :gasgn, exp
end

#process_iasgn(exp) ⇒ Object



344
345
346
# File 'lib/heckle.rb', line 344

def process_iasgn(exp)
  process_asgn :iasgn, exp
end

#process_if(exp) ⇒ Object



405
406
407
# 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) ⇒ Object

So process_call works correctly



279
280
281
282
283
284
285
# 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) ⇒ Object



364
365
366
# File 'lib/heckle.rb', line 364

def process_lasgn(exp)
  process_asgn :lasgn, exp
end

#process_lit(exp) ⇒ Object



374
375
376
# File 'lib/heckle.rb', line 374

def process_lit(exp)
  mutate_node s(:lit, exp.shift)
end

#process_str(exp) ⇒ Object



394
395
396
# File 'lib/heckle.rb', line 394

def process_str(exp)
  mutate_node s(:str, exp.shift)
end

#process_true(exp) ⇒ Object



416
417
418
# File 'lib/heckle.rb', line 416

def process_true(exp)
  mutate_node s(:true)
end

#process_until(exp) ⇒ Object



450
451
452
453
# 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) ⇒ Object



438
439
440
441
# 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_numberObject

Returns a random Fixnum.



609
610
611
# File 'lib/heckle.rb', line 609

def rand_number
  (rand(100) + 1)*((-1)**rand(2))
end

#rand_rangeObject

Returns a random Range



636
637
638
639
640
# File 'lib/heckle.rb', line 636

def rand_range
  min = rand(50)
  max = min + rand(50)
  min..max
end

#rand_stringObject

Returns a random String



616
617
618
619
620
621
# File 'lib/heckle.rb', line 616

def rand_string
  size = rand(50)
  str = ""
  size.times { str << rand(126).chr }
  str
end

#rand_symbolObject

Returns a random Symbol



626
627
628
629
630
631
# 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_mutationObject



201
202
203
# File 'lib/heckle.rb', line 201

def record_passing_mutation
  @failures << current_code
end

#resetObject



511
512
513
514
515
# File 'lib/heckle.rb', line 511

def reset
  reset_tree
  reset_mutatees
  mutation_count.clear
end

#reset_mutateesObject



533
534
535
# File 'lib/heckle.rb', line 533

def reset_mutatees
  @mutatees = @original_mutatees.deep_clone
end

#reset_treeObject



517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# 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_testsObject



147
148
149
150
151
152
153
# 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) ⇒ Boolean

Returns:

  • (Boolean)


556
557
558
559
560
561
562
# 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_streamObject

Suppresses output on $stdout and $stderr.



645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# 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?Boolean

Overwrite test_pass? for your own Heckle runner.

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)


143
144
145
# File 'lib/heckle.rb', line 143

def tests_pass?
  raise NotImplementedError
end

#validateObject

Running the script



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# 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) ⇒ Object

Tree operations



477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# 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