heckle 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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