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

Constant Summary collapse

VERSION =

The version of Heckle you are using.

'1.4.0'
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.



99
100
101
102
103
104
105
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
# File 'lib/heckle.rb', line 99

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 = Array

  @mutatees = Hash.new
  @mutation_count = Hash.new
  @node_count = Hash.new
  @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



46
47
48
# File 'lib/heckle.rb', line 46

def count
  @count
end

#failuresObject

Mutations that caused failures



51
52
53
# File 'lib/heckle.rb', line 51

def failures
  @failures
end

#klassObject

Class being heckled



56
57
58
# File 'lib/heckle.rb', line 56

def klass
  @klass
end

#klass_nameObject

Name of class being heckled



61
62
63
# File 'lib/heckle.rb', line 61

def klass_name
  @klass_name
end

#methodObject

Method being heckled



66
67
68
# File 'lib/heckle.rb', line 66

def method
  @method
end

#method_nameObject

Name of method being heckled



71
72
73
# File 'lib/heckle.rb', line 71

def method_name
  @method_name
end

#mutateesObject

:nodoc:



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

def mutatees
  @mutatees
end

#mutation_countObject

:nodoc:



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

def mutation_count
  @mutation_count
end

#node_countObject

:nodoc:



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

def node_count
  @node_count
end

#original_treeObject

:nodoc:



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

def original_tree
  @original_tree
end

Class Method Details

.debug=(value) ⇒ Object



82
83
84
# File 'lib/heckle.rb', line 82

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

.guess_timeout?Boolean

Returns:

  • (Boolean)


91
92
93
# File 'lib/heckle.rb', line 91

def self.guess_timeout?
  @@guess_timeout
end

.timeout=(value) ⇒ Object



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

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



504
505
506
# File 'lib/heckle.rb', line 504

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

#already_mutated?Boolean

Returns:

  • (Boolean)


522
523
524
# File 'lib/heckle.rb', line 522

def already_mutated?
  @mutated
end

#current_codeObject



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

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

#current_treeObject



448
449
450
# File 'lib/heckle.rb', line 448

def current_tree
  ParseTree.translate(klass_name.to_class, method_name)
end

#grab_conditional_loop_parts(exp) ⇒ Object



515
516
517
518
519
520
# File 'lib/heckle.rb', line 515

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



444
445
446
# File 'lib/heckle.rb', line 444

def grab_mutatees
  walk_and_push(current_tree)
end

#heckle(exp) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/heckle.rb', line 191

def heckle(exp)
  exp_copy = exp.deep_clone
  src = begin
          RubyToRuby.new.process(exp)
        rescue => e
          puts "Error: #{e.message} with: #{klass_name}##{method_name}: #{exp_copy.inspect}"
          raise e
        end

  original = RubyToRuby.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



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

def increment_mutation_count(node)
  # So we don't re-mutate this later if the tree is reset
  mutation_count[node] += 1
  @mutatees[node.first].delete_at(@mutatees[node.first].index(node))
  @mutated = true
end

#increment_node_count(node) ⇒ Object



486
487
488
489
490
491
492
# File 'lib/heckle.rb', line 486

def increment_node_count(node)
  if node_count[node].nil?
    node_count[node] = 1
  else
    node_count[node] += 1
  end
end

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



257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/heckle.rb', line 257

def mutate_asgn(node)
  type = node.shift
  var = node.shift
  if node.empty? then
    [:lasgn, :_heckle_dummy]
  else
    if node.last.first == :nil then
      [type, var, [:lit, 42]]
    else
      [type, var, [:nil]]
    end
  end
end

#mutate_call(node) ⇒ Object

Replaces the call node with nil.



232
233
234
# File 'lib/heckle.rb', line 232

def mutate_call(node)
  [:nil]
end

#mutate_false(node) ⇒ Object

Swaps for a :true node.



391
392
393
# File 'lib/heckle.rb', line 391

def mutate_false(node)
  [:true]
end

#mutate_if(node) ⇒ Object

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



369
370
371
# File 'lib/heckle.rb', line 369

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

#mutate_lit(exp) ⇒ Object

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



338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/heckle.rb', line 338

def mutate_lit(exp)
  case exp[1]
  when Fixnum, Float, Bignum
    [:lit, exp[1] + rand_number]
  when Symbol
    [:lit, rand_symbol]
  when Regexp
    [:lit, Regexp.new(Regexp.escape(rand_string.gsub(/\//, '\\/')))]
  when Range
    [:lit, rand_range]
  end
end

#mutate_node(node) ⇒ Object

Raises:

  • (UnsupportedNodeError)


419
420
421
422
423
424
425
426
427
428
429
# File 'lib/heckle.rb', line 419

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.



358
359
360
# File 'lib/heckle.rb', line 358

def mutate_str(node)
  [:str, rand_string]
end

#mutate_true(node) ⇒ Object

Swaps for a :false node.



380
381
382
# File 'lib/heckle.rb', line 380

def mutate_true(node)
  [:false]
end

#mutate_until(node) ⇒ Object

Swaps for a :while node.



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

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

#mutate_while(node) ⇒ Object

Swaps for a :until node.



403
404
405
# File 'lib/heckle.rb', line 403

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

#mutations_leftObject



526
527
528
529
530
# File 'lib/heckle.rb', line 526

def mutations_left
  sum = 0
  @mutatees.each {|mut| sum += mut.last.size }
  sum
end

#process_asgn(type, exp) ⇒ Object



248
249
250
251
252
253
254
255
# File 'lib/heckle.rb', line 248

def process_asgn(type, exp)
  var = exp.shift
  if exp.empty? then
    mutate_node [type, var]
  else
    mutate_node [type, var, process(exp.shift)]
  end
end

#process_call(exp) ⇒ Object

Processing sexps



218
219
220
221
222
223
224
225
226
227
# File 'lib/heckle.rb', line 218

def process_call(exp)
  recv = process(exp.shift)
  meth = exp.shift
  args = process(exp.shift)

  out = [:call, recv, meth]
  out << args if args

  mutate_node out
end

#process_cvasgn(exp) ⇒ Object



271
272
273
# File 'lib/heckle.rb', line 271

def process_cvasgn(exp)
  process_asgn :cvasgn, exp
end

#process_dasgn(exp) ⇒ Object



281
282
283
# File 'lib/heckle.rb', line 281

def process_dasgn(exp)
  process_asgn :dasgn, exp
end

#process_dasgn_curr(exp) ⇒ Object



291
292
293
# File 'lib/heckle.rb', line 291

def process_dasgn_curr(exp)
  process_asgn :dasgn_curr, exp
end

#process_defn(exp) ⇒ Object



236
237
238
239
240
241
242
243
244
245
246
# File 'lib/heckle.rb', line 236

def process_defn(exp)
  self.method = exp.shift
  result = [:defn, method]
  result << process(exp.shift) until exp.empty?
  heckle(result) if method == method_name

  return result
ensure
  @mutated = false
  reset_node_count
end

#process_false(exp) ⇒ Object



384
385
386
# File 'lib/heckle.rb', line 384

def process_false(exp)
  mutate_node [:false]
end

#process_gasgn(exp) ⇒ Object



311
312
313
# File 'lib/heckle.rb', line 311

def process_gasgn(exp)
  process_asgn :gasgn, exp
end

#process_iasgn(exp) ⇒ Object



301
302
303
# File 'lib/heckle.rb', line 301

def process_iasgn(exp)
  process_asgn :iasgn, exp
end

#process_if(exp) ⇒ Object



362
363
364
# File 'lib/heckle.rb', line 362

def process_if(exp)
  mutate_node [:if, process(exp.shift), process(exp.shift), process(exp.shift)]
end

#process_lasgn(exp) ⇒ Object



321
322
323
# File 'lib/heckle.rb', line 321

def process_lasgn(exp)
  process_asgn :lasgn, exp
end

#process_lit(exp) ⇒ Object



331
332
333
# File 'lib/heckle.rb', line 331

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

#process_str(exp) ⇒ Object



351
352
353
# File 'lib/heckle.rb', line 351

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

#process_true(exp) ⇒ Object



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

def process_true(exp)
  mutate_node [:true]
end

#process_until(exp) ⇒ Object



407
408
409
410
# File 'lib/heckle.rb', line 407

def process_until(exp)
  cond, body, head_controlled = grab_conditional_loop_parts(exp)
  mutate_node [:until, cond, body, head_controlled]
end

#process_while(exp) ⇒ Object



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

def process_while(exp)
  cond, body, head_controlled = grab_conditional_loop_parts(exp)
  mutate_node [:while, cond, body, head_controlled]
end

#rand_numberObject

Returns a random Fixnum.



539
540
541
# File 'lib/heckle.rb', line 539

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

#rand_rangeObject

Returns a random Range



566
567
568
569
570
# File 'lib/heckle.rb', line 566

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

#rand_stringObject

Returns a random String



546
547
548
549
550
551
# File 'lib/heckle.rb', line 546

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

#rand_symbolObject

Returns a random Symbol



556
557
558
559
560
561
# File 'lib/heckle.rb', line 556

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



187
188
189
# File 'lib/heckle.rb', line 187

def record_passing_mutation
  @failures << current_code
end

#resetObject



452
453
454
455
456
# File 'lib/heckle.rb', line 452

def reset
  reset_tree
  reset_mutatees
  reset_mutation_count
end

#reset_mutateesObject



474
475
476
# File 'lib/heckle.rb', line 474

def reset_mutatees
  @mutatees = @original_mutatees.deep_clone
end

#reset_mutation_countObject



478
479
480
# File 'lib/heckle.rb', line 478

def reset_mutation_count
  mutation_count.each {|k,v| mutation_count[k] = 0}
end

#reset_node_countObject



482
483
484
# File 'lib/heckle.rb', line 482

def reset_node_count
  node_count.each {|k,v| node_count[k] = 0}
end

#reset_treeObject



458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/heckle.rb', line 458

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



140
141
142
143
144
145
146
# File 'lib/heckle.rb', line 140

def run_tests
  if tests_pass? then
    record_passing_mutation
  else
    @reporter.report_test_failures
  end
end

#should_heckle?(exp) ⇒ Boolean

Returns:

  • (Boolean)


508
509
510
511
512
513
# File 'lib/heckle.rb', line 508

def should_heckle?(exp)
  return false unless method == method_name
  mutation_count[exp] = 0 if mutation_count[exp].nil?
  return false if node_count[exp] <= mutation_count[exp]
  ( mutatees[exp.first.to_sym] || [] ).include?(exp) && !already_mutated?
end

#silence_streamObject

Suppresses output on $stdout and $stderr.



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
# File 'lib/heckle.rb', line 575

def silence_stream
  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

#tests_pass?Boolean

Overwrite test_pass? for your own Heckle runner.

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)


136
137
138
# File 'lib/heckle.rb', line 136

def tests_pass?
  raise NotImplementedError
end

#validateObject

Running the script



151
152
153
154
155
156
157
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
# File 'lib/heckle.rb', line 151

def validate
  if mutations_left == 0
    @reporter.no_mutations(method_name)
    return
  end

  @reporter.method_loaded(klass_name, method_name, mutations_left)

  until mutations_left == 0
    @reporter.remaining_mutations(mutations_left)
    reset_tree
    begin
      process current_tree
      silence_stream { timeout(@@timeout) { run_tests } }
    rescue SyntaxError => e
      @reporter.warning "Mutation caused a syntax error:\n\n#{e.message}}"
    rescue Timeout::Error
      @reporter.warning "Your tests timed out. Heckle may have caused an infinite loop."
    end
  end

  reset # in case we're validating again. we should clean up.

  unless @failures.empty?
    @reporter.no_failures
    @failures.each do |failure|
      original = RubyToRuby.new.process(@original_tree.deep_clone)
      @reporter.failure(original, failure)
    end
    false
  else
    @reporter.no_surviving_mutants
    true
  end
end

#walk_and_push(node) ⇒ Object

Tree operations



434
435
436
437
438
439
440
441
442
# File 'lib/heckle.rb', line 434

def walk_and_push(node)
  return unless node.respond_to? :each
  return if node.is_a? String
  node.each { |child| walk_and_push(child) }
  if @mutatable_nodes.include? node.first
    @mutatees[node.first.to_sym].push(node)
    mutation_count[node] = 0
  end
end