heckle 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt CHANGED
@@ -1,3 +1,15 @@
1
+ == 1.2.0 / 2007-01-15
2
+
3
+ * 2 major enhancements:
4
+ * Timeout for tests set dynamically and overridable with -T
5
+ * Class method support with "self.method_name"
6
+ * 3 minor enhancements:
7
+ * -b allows heckling of branches only
8
+ * Restructured class heirarchy and got rid of Base and others.
9
+ * Revamped the tests and reduced size by 60%.
10
+ * 1 bug fix:
11
+ * Fixed the infinite loop caused by syntax errors
12
+
1
13
  == 1.1.1 / 2006-12-20
2
14
 
3
15
  * 3 bug fixes:
data/Manifest.txt CHANGED
@@ -4,8 +4,6 @@ README.txt
4
4
  Rakefile
5
5
  bin/heckle
6
6
  lib/heckle.rb
7
- lib/heckle/base.rb
8
- lib/heckle/reporter.rb
9
7
  lib/test_unit_heckler.rb
10
8
  sample/Rakefile
11
9
  sample/changes.log
data/bin/heckle CHANGED
@@ -10,11 +10,31 @@ opts = OptionParser.new do |opts|
10
10
  TestUnitHeckler.debug = true
11
11
  end
12
12
 
13
- opts.on( "-t", "--tests TEST_PATTERN",
13
+ opts.on( "-V", "--version", "Prints Heckle's version number") do |opt|
14
+ puts "Heckle #{Heckle::VERSION}"
15
+ exit 0
16
+ end
17
+
18
+ opts.on( "-t", "--tests TEST_PATTERN",
14
19
  "Location of tests (glob)" ) do |pattern|
15
20
  TestUnitHeckler.test_pattern = pattern
16
21
  end
22
+
23
+ opts.on( "-b", "--branches-only", "Only mutate branches" ) do |opt|
24
+ puts "!"*70
25
+ puts "!!! Heckling branches only"
26
+ puts "!"*70
27
+ puts
28
+
29
+ Heckle::MUTATABLE_NODES.replace [:if, :while, :until]
30
+ end
17
31
 
32
+ opts.on( "-T", "--timeout SECONDS", "The maximum time for a test run in seconds",
33
+ "Used to catch infinite loops") do |timeout|
34
+ Heckle.timeout = timeout.to_i
35
+ puts "Setting timeout at #{timeout} seconds."
36
+ end
37
+
18
38
  opts.on( "-h", "--help", "Show this message") do |opt|
19
39
  puts opts
20
40
  exit 0
@@ -32,4 +52,3 @@ unless impl then
32
52
  end
33
53
 
34
54
  TestUnitHeckler.validate(impl, meth)
35
-
data/lib/heckle.rb CHANGED
@@ -1,3 +1,442 @@
1
1
  require 'rubygems'
2
- require 'heckle/reporter'
3
- require 'heckle/base'
2
+ require 'parse_tree'
3
+ require 'ruby2ruby'
4
+ require 'timeout'
5
+
6
+ class String
7
+ def to_class
8
+ split(/::/).inject(Object) { |klass, name| klass.const_get(name) }
9
+ end
10
+ end
11
+
12
+ class Heckle < SexpProcessor
13
+ VERSION = '1.2.0'
14
+ MUTATABLE_NODES = [:if, :lit, :str, :true, :false, :while, :until]
15
+ WINDOZE = RUBY_PLATFORM =~ /mswin/
16
+ NULL_PATH = WINDOZE ? 'NUL:' : '/dev/null'
17
+
18
+ attr_accessor(:klass_name, :method_name, :klass, :method, :mutatees,
19
+ :original_tree, :mutation_count, :node_count,
20
+ :failures, :count)
21
+
22
+ @@debug = false
23
+ @@guess_timeout = true
24
+ @@timeout = 60 # default to something longer (can be overridden by runners)
25
+
26
+ def self.debug=(value)
27
+ @@debug = value
28
+ end
29
+
30
+ def self.timeout=(value)
31
+ @@timeout = value
32
+ @@guess_timeout = false # We've set the timeout, don't guess
33
+ end
34
+
35
+ def self.guess_timeout?
36
+ @@guess_timeout
37
+ end
38
+
39
+ def initialize(klass_name=nil, method_name=nil, reporter = Reporter.new)
40
+ super()
41
+
42
+ @klass_name = klass_name
43
+ @method_name = method_name.intern if method_name
44
+
45
+ @klass = klass_name.to_class
46
+
47
+ @method = nil
48
+ @reporter = reporter
49
+
50
+ self.strict = false
51
+ self.auto_shift_type = true
52
+ self.expected = Array
53
+
54
+ @mutatees = Hash.new
55
+ @mutation_count = Hash.new
56
+ @node_count = Hash.new
57
+ @count = 0
58
+
59
+ MUTATABLE_NODES.each {|type| @mutatees[type] = [] }
60
+
61
+ @failures = []
62
+
63
+ @mutated = false
64
+
65
+ grab_mutatees
66
+
67
+ @original_tree = current_tree.deep_clone
68
+ @original_mutatees = mutatees.deep_clone
69
+ end
70
+
71
+ ############################################################
72
+ ### Overwrite test_pass? for your own Heckle runner.
73
+ def tests_pass?
74
+ raise NotImplementedError
75
+ end
76
+
77
+ def run_tests
78
+ if tests_pass? then
79
+ record_passing_mutation
80
+ else
81
+ @reporter.report_test_failures
82
+ end
83
+ end
84
+
85
+ ############################################################
86
+ ### Running the script
87
+
88
+ def validate
89
+ if mutations_left == 0
90
+ @reporter.no_mutations(method_name)
91
+ return
92
+ end
93
+
94
+ @reporter.method_loaded(klass_name, method_name, mutations_left)
95
+
96
+ until mutations_left == 0
97
+ @reporter.remaining_mutations(mutations_left)
98
+ reset_tree
99
+ begin
100
+ process current_tree
101
+ silence_stream { timeout(@@timeout) { run_tests } }
102
+ rescue SyntaxError => e
103
+ @reporter.warning "Mutation caused a syntax error:\n\n#{e.message}}"
104
+ rescue Timeout::Error
105
+ @reporter.warning "Your tests timed out. Heckle may have caused an infinite loop."
106
+ end
107
+ end
108
+
109
+ reset # in case we're validating again. we should clean up.
110
+
111
+ unless @failures.empty?
112
+ @reporter.no_failures
113
+ @failures.each do |failure|
114
+ @reporter.failure(failure)
115
+ end
116
+ else
117
+ @reporter.no_surviving_mutants
118
+ end
119
+ end
120
+
121
+ def record_passing_mutation
122
+ @failures << current_code
123
+ end
124
+
125
+ def heckle(exp)
126
+ orig_exp = exp.deep_clone
127
+ src = begin
128
+ RubyToRuby.new.process(exp)
129
+ rescue => e
130
+ puts "Error: #{e.message} with: #{klass_name}##{method_name}: #{orig_exp.inspect}"
131
+ raise e
132
+ end
133
+ @reporter.replacing(klass_name, method_name, src) if @@debug
134
+
135
+ clean_name = method_name.to_s.gsub(/self\./, '')
136
+ self.count += 1
137
+ new_name = "h#{count}_#{clean_name}"
138
+
139
+ klass = aliasing_class method_name
140
+ klass.send :remove_method, new_name rescue nil
141
+ klass.send :alias_method, new_name, clean_name
142
+ klass.send :remove_method, clean_name rescue nil
143
+
144
+ @klass.class_eval src, "(#{new_name})"
145
+ end
146
+
147
+ ############################################################
148
+ ### Processing sexps
149
+
150
+ def process_defn(exp)
151
+ self.method = exp.shift
152
+ result = [:defn, method]
153
+ result << process(exp.shift) until exp.empty?
154
+ heckle(result) if method == method_name
155
+
156
+ return result
157
+ ensure
158
+ @mutated = false
159
+ reset_node_count
160
+ end
161
+
162
+ def process_lit(exp)
163
+ mutate_node [:lit, exp.shift]
164
+ end
165
+
166
+ def mutate_lit(exp)
167
+ case exp[1]
168
+ when Fixnum, Float, Bignum
169
+ [:lit, exp[1] + rand_number]
170
+ when Symbol
171
+ [:lit, rand_symbol]
172
+ when Regexp
173
+ [:lit, /#{Regexp.escape(rand_string)}/]
174
+ when Range
175
+ [:lit, rand_range]
176
+ end
177
+ end
178
+
179
+ def process_str(exp)
180
+ mutate_node [:str, exp.shift]
181
+ end
182
+
183
+ def mutate_str(node)
184
+ [:str, rand_string]
185
+ end
186
+
187
+ def process_if(exp)
188
+ mutate_node [:if, process(exp.shift), process(exp.shift), process(exp.shift)]
189
+ end
190
+
191
+ def mutate_if(node)
192
+ [:if, node[1], node[3], node[2]]
193
+ end
194
+
195
+ def process_true(exp)
196
+ mutate_node [:true]
197
+ end
198
+
199
+ def mutate_true(node)
200
+ [:false]
201
+ end
202
+
203
+ def process_false(exp)
204
+ mutate_node [:false]
205
+ end
206
+
207
+ def mutate_false(node)
208
+ [:true]
209
+ end
210
+
211
+ def process_while(exp)
212
+ cond, body, head_controlled = grab_conditional_loop_parts(exp)
213
+ mutate_node [:while, cond, body, head_controlled]
214
+ end
215
+
216
+ def mutate_while(node)
217
+ [:until, node[1], node[2], node[3]]
218
+ end
219
+
220
+ def process_until(exp)
221
+ cond, body, head_controlled = grab_conditional_loop_parts(exp)
222
+ mutate_node [:until, cond, body, head_controlled]
223
+ end
224
+
225
+ def mutate_until(node)
226
+ [:while, node[1], node[2], node[3]]
227
+ end
228
+
229
+ def mutate_node(node)
230
+ raise UnsupportedNodeError unless respond_to? "mutate_#{node.first}"
231
+ increment_node_count node
232
+
233
+ if should_heckle? node
234
+ increment_mutation_count node
235
+ return send("mutate_#{node.first}", node)
236
+ else
237
+ node
238
+ end
239
+ end
240
+
241
+ ############################################################
242
+ ### Tree operations
243
+
244
+ def walk_and_push(node)
245
+ return unless node.respond_to? :each
246
+ return if node.is_a? String
247
+ node.each { |child| walk_and_push(child) }
248
+ if MUTATABLE_NODES.include? node.first
249
+ @mutatees[node.first.to_sym].push(node)
250
+ mutation_count[node] = 0
251
+ end
252
+ end
253
+
254
+ def grab_mutatees
255
+ walk_and_push(current_tree)
256
+ end
257
+
258
+ def current_tree
259
+ ParseTree.translate(klass_name.to_class, method_name)
260
+ end
261
+
262
+ def reset
263
+ reset_tree
264
+ reset_mutatees
265
+ reset_mutation_count
266
+ end
267
+
268
+ def reset_tree
269
+ return unless original_tree != current_tree
270
+ @mutated = false
271
+
272
+ self.count += 1
273
+
274
+ clean_name = method_name.to_s.gsub(/self\./, '')
275
+ new_name = "h#{count}_#{clean_name}"
276
+
277
+ klass = aliasing_class method_name
278
+
279
+ klass.send :undef_method, new_name rescue nil
280
+ klass.send :alias_method, new_name, clean_name
281
+ klass.send :alias_method, clean_name, "h1_#{clean_name}"
282
+ end
283
+
284
+ def reset_mutatees
285
+ @mutatees = @original_mutatees.deep_clone
286
+ end
287
+
288
+ def reset_mutation_count
289
+ mutation_count.each {|k,v| mutation_count[k] = 0}
290
+ end
291
+
292
+ def reset_node_count
293
+ node_count.each {|k,v| node_count[k] = 0}
294
+ end
295
+
296
+ def increment_node_count(node)
297
+ if node_count[node].nil?
298
+ node_count[node] = 1
299
+ else
300
+ node_count[node] += 1
301
+ end
302
+ end
303
+
304
+ def increment_mutation_count(node)
305
+ # So we don't re-mutate this later if the tree is reset
306
+ mutation_count[node] += 1
307
+ @mutatees[node.first].delete_at(@mutatees[node.first].index(node))
308
+ @mutated = true
309
+ end
310
+
311
+ ############################################################
312
+ ### Convenience methods
313
+
314
+ def aliasing_class(method_name)
315
+ method_name.to_s =~ /self\./ ? class << @klass; self; end : @klass
316
+ end
317
+
318
+ def should_heckle?(exp)
319
+ return false unless method == method_name
320
+ mutation_count[exp] = 0 if mutation_count[exp].nil?
321
+ return false if node_count[exp] <= mutation_count[exp]
322
+ ( mutatees[exp.first.to_sym] || [] ).include?(exp) && !already_mutated?
323
+ end
324
+
325
+ def grab_conditional_loop_parts(exp)
326
+ cond = process(exp.shift)
327
+ body = process(exp.shift)
328
+ head_controlled = exp.shift
329
+ return cond, body, head_controlled
330
+ end
331
+
332
+ def already_mutated?
333
+ @mutated
334
+ end
335
+
336
+ def mutations_left
337
+ sum = 0
338
+ @mutatees.each {|mut| sum += mut.last.size }
339
+ sum
340
+ end
341
+
342
+ def current_code
343
+ RubyToRuby.translate(klass_name.to_class, method_name)
344
+ end
345
+
346
+ def rand_number
347
+ (rand(10) + 1)*((-1)**rand(2))
348
+ end
349
+
350
+ def rand_string
351
+ size = rand(50)
352
+ str = ""
353
+ size.times { str << rand(126).chr }
354
+ str
355
+ end
356
+
357
+ def rand_symbol
358
+ letters = ('a'..'z').to_a + ('A'..'Z').to_a
359
+ str = ""
360
+ (rand(50) + 1).times { str << letters[rand(letters.size)] }
361
+ :"#{str}"
362
+ end
363
+
364
+ def rand_range
365
+ min = rand(50)
366
+ max = min + rand(50)
367
+ min..max
368
+ end
369
+
370
+ def silence_stream
371
+ dead = File.open("/dev/null", "w")
372
+
373
+ $stdout.flush
374
+ $stderr.flush
375
+
376
+ oldstdout = $stdout.dup
377
+ oldstderr = $stderr.dup
378
+
379
+ $stdout.reopen(dead)
380
+ $stderr.reopen(dead)
381
+
382
+ result = yield
383
+
384
+ ensure
385
+ $stdout.flush
386
+ $stderr.flush
387
+
388
+ $stdout.reopen(oldstdout)
389
+ $stderr.reopen(oldstderr)
390
+ result
391
+ end
392
+
393
+ class Reporter
394
+ def no_mutations(method_name)
395
+ warning "#{method_name} has a thick skin. There's nothing to heckle."
396
+ end
397
+
398
+ def method_loaded(klass_name, method_name, mutations_left)
399
+ info "#{klass_name}\##{method_name} loaded with #{mutations_left} possible mutations"
400
+ end
401
+
402
+ def remaining_mutations(mutations_left)
403
+ puts "#{mutations_left} mutations remaining..."
404
+ end
405
+
406
+ def warning(message)
407
+ puts
408
+ puts "!" * 70
409
+ puts "!!! #{message}"
410
+ puts "!" * 70
411
+ puts
412
+ end
413
+
414
+ def info(message)
415
+ puts
416
+ puts "*"*70
417
+ puts "*** #{message}"
418
+ puts "*"*70
419
+ puts
420
+ end
421
+
422
+ def no_failures
423
+ puts "\nThe following mutations didn't cause test failures:\n"
424
+ end
425
+
426
+ def failure(failure)
427
+ puts "\n#{failure}\n"
428
+ end
429
+
430
+ def no_surviving_mutants
431
+ puts "No mutants survived. Cool!\n\n"
432
+ end
433
+
434
+ def replacing(klass_name, method_name, src)
435
+ puts "Replacing #{klass_name}##{method_name} with:\n\n#{src}\n"
436
+ end
437
+
438
+ def report_test_failures
439
+ puts "Tests failed -- this is good"
440
+ end
441
+ end
442
+ end
@@ -4,14 +4,14 @@ require 'test/unit/autorunner'
4
4
  require 'heckle'
5
5
  $: << 'lib' << 'test'
6
6
 
7
- class TestUnitHeckler < Heckle::Base
7
+ class TestUnitHeckler < Heckle
8
8
  @@test_pattern = 'test/test_*.rb'
9
9
  @@tests_loaded = false;
10
10
 
11
11
  def self.test_pattern=(value)
12
12
  @@test_pattern = value
13
13
  end
14
-
14
+
15
15
  def self.load_test_files
16
16
  @@tests_loaded = true
17
17
  Dir.glob(@@test_pattern).each {|test| require test}
@@ -21,13 +21,30 @@ class TestUnitHeckler < Heckle::Base
21
21
  load_test_files
22
22
  klass = klass_name.to_class
23
23
 
24
- if method_name
25
- self.new(klass_name, method_name).test_and_validate
26
- else
27
- klass.instance_methods(false).each do |method_name|
28
- heckler = self.new(klass_name, method_name)
29
- heckler.test_and_validate
30
- end
24
+ initial_time = Time.now
25
+
26
+ unless self.new(klass_name).tests_pass? then
27
+ abort "Initial run of tests failed... fix and run heckle again"
28
+ end
29
+
30
+ if self.guess_timeout?
31
+ running_time = (Time.now - initial_time)
32
+ adjusted_timeout = (running_time * 2 < 5) ? 5 : (running_time * 2)
33
+ self.timeout = adjusted_timeout
34
+ puts "Setting timeout at #{adjusted_timeout} seconds." if @@debug
35
+
36
+ end
37
+
38
+ puts "Initial tests pass. Let's rumble."
39
+ self.timeout = adjusted_timeout
40
+
41
+ puts "Initial tests pass. Let's rumble."
42
+
43
+ klass_methods = klass.singleton_methods(false).collect {|meth| "self.#{meth}"}
44
+ methods = method_name ? Array(method_name) : klass.instance_methods(false) + klass_methods
45
+
46
+ methods.each do |method_name|
47
+ self.new(klass_name, method_name).validate
31
48
  end
32
49
  end
33
50
 
@@ -35,17 +52,10 @@ class TestUnitHeckler < Heckle::Base
35
52
  super(klass_name, method_name)
36
53
  self.class.load_test_files unless @@tests_loaded
37
54
  end
38
-
39
- def test_and_validate
40
- if silence_stream(STDOUT) { tests_pass? } then
41
- puts "Initial tests pass. Let's rumble."
42
- validate
43
- else
44
- puts "Tests failed... fix and run heckle again"
45
- end
46
- end
47
-
55
+
48
56
  def tests_pass?
49
- Test::Unit::AutoRunner.run
57
+ silence_stream do
58
+ Test::Unit::AutoRunner.run
59
+ end
50
60
  end
51
61
  end
@@ -5,6 +5,10 @@ class Heckled
5
5
  @names = []
6
6
  end
7
7
 
8
+ def self.is_a_klass_method?
9
+ true
10
+ end
11
+
8
12
  def uses_while
9
13
  i = 1
10
14
  while i < 10
@@ -64,4 +68,13 @@ class Heckled
64
68
  def uses_masignment
65
69
  one, two = [1, 2]
66
70
  end
71
+
72
+ def uses_infinite_loop?
73
+ # Converts to a infinite loop actually
74
+ some_func until true
75
+ end
76
+
77
+ # placeholder
78
+ def some_func
79
+ end
67
80
  end
@@ -16,4 +16,12 @@ class TestHeckled < Test::Unit::TestCase
16
16
  @heckled.uses_strings
17
17
  assert_equal ["Hello, Robert", "Hello, Jeff", "Hi, Frank"], @heckled.names
18
18
  end
19
+
20
+ def test_uses_infinite_loop
21
+ @heckled.uses_infinite_loop?
22
+ end
23
+
24
+ def test_is_a_klass_method
25
+ assert_equal true, Heckled.is_a_klass_method?
26
+ end
19
27
  end