heckle 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,19 @@
1
+ == 1.1.0 / 2006-12-19
2
+
3
+ * 12 major enhancements:
4
+ * Able to roll back original method after processing.
5
+ * Can mutate numeric literals.
6
+ * Can mutate strings.
7
+ * Can mutate a node at a time.
8
+ * Can mutate if/unless
9
+ * Decoupled from Test::Unit
10
+ * Cleaner output
11
+ * Can mutate true and false.
12
+ * Can mutate while and until.
13
+ * Can mutate regexes, ranges, symbols
14
+ * Can run against entire classes
15
+ * Command line options!
16
+
1
17
  == 1.0.0 / 2006-10-22
2
18
 
3
19
  * 1 major enhancement
@@ -4,5 +4,12 @@ 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
+ lib/test_unit_heckler.rb
10
+ sample/Rakefile
11
+ sample/changes.log
12
+ sample/lib/heckled.rb
13
+ sample/test/test_heckled.rb
14
+ test/fixtures/heckled.rb
7
15
  test/test_heckle.rb
8
-
data/README.txt CHANGED
@@ -1,14 +1,14 @@
1
1
  heckle
2
- by Ryan Davis
3
2
  http://www.rubyforge.org/projects/seattlerb
3
+ by Ryan Davis and Kevin Clark
4
4
 
5
5
  == DESCRIPTION:
6
-
6
+
7
7
  Unit Testing Sadism. More description coming soon. I'm punting to get
8
8
  this launched ASAP.
9
9
 
10
10
  == FEATURES/PROBLEMS:
11
-
11
+
12
12
  * needs some love. haha.
13
13
 
14
14
  == SYNOPSYS:
@@ -27,7 +27,7 @@ this launched ASAP.
27
27
 
28
28
  (The MIT License)
29
29
 
30
- Copyright (c) 2006 FIX
30
+ Copyright (c) 2006 Ryan Davis and Kevin Clark
31
31
 
32
32
  Permission is hereby granted, free of charge, to any person obtaining
33
33
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -1,5 +1,7 @@
1
1
  # -*- ruby -*-
2
2
 
3
+ $: << 'lib'
4
+
3
5
  require 'rubygems'
4
6
  require 'hoe'
5
7
  require './lib/heckle.rb'
@@ -7,9 +9,11 @@ require './lib/heckle.rb'
7
9
  Hoe.new('heckle', Heckle::VERSION) do |p|
8
10
  p.rubyforge_name = 'seattlerb'
9
11
  p.summary = 'Unit Test Sadism'
10
- p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
12
+ p.description = p.paragraphs_of('README.txt', 2).join("\n\n")
11
13
  p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
12
14
  p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+
16
+ p.extra_deps << ['ruby2ruby', '>= 1.1.0']
13
17
  end
14
18
 
15
19
  # vim: syntax=Ruby
data/bin/heckle CHANGED
@@ -1,15 +1,37 @@
1
- #!/usr/local/bin/ruby -w
1
+ #!/usr/local/bin/ruby
2
2
 
3
- $: << 'lib'
4
- require 'heckle'
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+ require 'test_unit_heckler'
5
+ require 'optparse'
6
+
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: #{File.basename($0)} class_name [method_name]"
9
+ opts.on( "-v", "--verbose", "Loudly explain heckle run" ) do |opt|
10
+ TestUnitHeckler.debug = true
11
+ end
12
+
13
+ opts.on( "-t", "--tests TEST_PATTERN",
14
+ "Location of tests (glob)" ) do |pattern|
15
+ TestUnitHeckler.test_pattern = pattern
16
+ end
17
+
18
+ opts.on( "-h", "--help", "Show this message") do |opt|
19
+ puts opts
20
+ end
21
+ end
22
+
23
+ opts.parse!
5
24
 
6
- file = ARGV.shift
7
25
  impl = ARGV.shift
8
26
  meth = ARGV.shift
9
27
 
10
- unless file and impl and meth then
11
- abort "usage: #{File.basename($0)} file.rb impl_class_name impl_method_name"
28
+ unless impl then
29
+ puts opts
30
+ exit 1
12
31
  end
13
32
 
14
- heckle = Heckle.new(file, impl, meth)
15
- heckle.validate
33
+ if meth
34
+ TestUnitHeckler.new(impl, meth).validate
35
+ else
36
+ TestUnitHeckler.validate(impl)
37
+ end
@@ -1,89 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'parse_tree'
3
3
  require 'ruby2ruby'
4
- require 'test/unit/autorunner'
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.0.0'
14
-
15
- attr_accessor :file, :klass_name, :method_name, :klass, :method
16
- def initialize(file=nil, klass_name=nil, method_name=nil)
17
- super()
18
-
19
- @file, @klass_name, @method_name = file, klass_name, method_name.intern
20
- @klass = @method = nil
21
-
22
- self.strict = false
23
- self.auto_shift_type = true
24
- self.expected = Array
25
- end
26
-
27
- def validate
28
- puts "Validating #{file}"
29
-
30
- load file
31
- if Test::Unit::AutoRunner.run then
32
- puts "Tests passed -- heckling"
33
-
34
- process(ParseTree.translate(klass_name.to_class, method_name))
35
-
36
- if Test::Unit::AutoRunner.run then
37
- puts
38
- abort "*** Tests passed again after heckling, your tests are incomplete"
39
- else
40
- puts "Tests failed -- this is good"
41
- end
42
- else
43
- puts "Tests failed... fix and run heckle again"
44
- end
45
- end
46
-
47
- ############################################################
48
-
49
- def process_defn(exp)
50
- self.method = exp.shift
51
- result = [:defn, method]
52
-
53
- result << process(exp.shift) until exp.empty?
54
-
55
- heckle(result) if should_heckle?
56
-
57
- return result
58
- end
59
-
60
- def process_if(exp)
61
- cond = process(exp.shift)
62
- t = process(exp.shift)
63
- f = process(exp.shift)
64
-
65
- if should_heckle? then
66
- [:if, cond, f, t]
67
- else
68
- [:if, cond, t, f]
69
- end
70
- end
71
-
72
- def should_heckle?
73
- method == method_name
74
- end
75
-
76
- def heckle(exp)
77
- puts "Heckling #{klass_name}##{method_name}"
78
-
79
- r2r = RubyToRuby.new
80
- src = r2r.process(exp)
81
-
82
- puts
83
- puts "Replacing #{klass}##{method_name} with:"
84
- puts src
85
- puts
86
-
87
- klass_name.to_class.class_eval(src)
88
- end
89
- end
4
+ require 'logger'
5
+ require 'heckle/reporter'
6
+ require 'heckle/base'
@@ -0,0 +1,349 @@
1
+ class String
2
+ def to_class
3
+ split(/::/).inject(Object) { |klass, name| klass.const_get(name) }
4
+ end
5
+ end
6
+
7
+ module Heckle
8
+ VERSION = '1.1.0'
9
+
10
+ class Base < SexpProcessor
11
+ MUTATABLE_NODES = [:if, :lit, :str, :true, :false, :while, :until]
12
+
13
+ attr_accessor :klass_name, :method_name, :klass, :method, :mutatees, :original_tree,
14
+ :mutation_count, :node_count, :failures, :count
15
+
16
+ @@debug = false;
17
+
18
+ def self.debug=(value)
19
+ @@debug = value
20
+ end
21
+
22
+ def initialize(klass_name=nil, method_name=nil, reporter = Reporter.new)
23
+ super()
24
+
25
+ @klass_name, @method_name = klass_name, method_name.intern
26
+ @klass = @method = nil
27
+ @reporter = reporter
28
+
29
+ self.strict = false
30
+ self.auto_shift_type = true
31
+ self.expected = Array
32
+
33
+ @mutatees = Hash.new
34
+ @mutation_count = Hash.new
35
+ @node_count = Hash.new
36
+ @count = 0
37
+
38
+ MUTATABLE_NODES.each {|type| @mutatees[type] = [] }
39
+
40
+ @failures = []
41
+
42
+ @mutated = false
43
+
44
+ grab_mutatees
45
+
46
+ @original_tree = current_tree.deep_clone
47
+ @original_mutatees = mutatees.deep_clone
48
+ end
49
+
50
+ ############################################################
51
+ ### Overwrite test_pass? for your own Heckle runner.
52
+ def tests_pass?
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def run_tests
57
+ if tests_pass? then
58
+ record_passing_mutation
59
+ else
60
+ @reporter.report_test_failures
61
+ end
62
+ end
63
+
64
+ ############################################################
65
+ ### Running the script
66
+
67
+ def validate
68
+ if mutations_left == 0
69
+ @reporter.no_mutations(method_name)
70
+ return
71
+ end
72
+
73
+ @reporter.method_loaded(klass_name, method_name, mutations_left)
74
+
75
+ until mutations_left == 0
76
+ @reporter.remaining_mutations(mutations_left)
77
+ reset_tree
78
+ begin
79
+ process current_tree
80
+ silence_stream(STDOUT) { run_tests }
81
+ rescue SyntaxError => e
82
+ puts "Mutation caused a syntax error: #{e.message}"
83
+ end
84
+ end
85
+
86
+ reset # in case we're validating again. we should clean up.
87
+
88
+ unless @failures.empty?
89
+ @reporter.no_failures
90
+ @failures.each do |failure|
91
+ @reporter.failure(failure)
92
+ end
93
+ else
94
+ @reporter.no_surviving_mutants
95
+ end
96
+ end
97
+
98
+ def record_passing_mutation
99
+ @failures << current_code
100
+ end
101
+
102
+ def heckle(exp)
103
+ src = RubyToRuby.new.process(exp)
104
+ @reporter.replacing(klass_name, method_name, src) if @@debug
105
+ klass = klass_name.to_class
106
+ self.count += 1
107
+ new_name = "#{method_name}_#{count}"
108
+
109
+ klass.send :undef_method, new_name rescue nil
110
+ klass.send :alias_method, new_name, method_name
111
+ klass.class_eval(src)
112
+ end
113
+
114
+ ############################################################
115
+ ### Processing sexps
116
+
117
+ def process_defn(exp)
118
+ self.method = exp.shift
119
+ result = [:defn, method]
120
+ result << process(exp.shift) until exp.empty?
121
+ heckle(result) if method == method_name
122
+ @mutated = false
123
+ reset_node_count
124
+
125
+ return result
126
+ end
127
+
128
+ def process_lit(exp)
129
+ mutate_node [:lit, exp.shift]
130
+ end
131
+
132
+ def mutate_lit(exp)
133
+ case exp[1]
134
+ when Fixnum, Float, Bignum
135
+ [:lit, exp[1] + rand_number]
136
+ when Symbol
137
+ [:lit, rand_symbol]
138
+ when Regexp
139
+ [:lit, /#{Regexp.escape(rand_string)}/]
140
+ when Range
141
+ [:lit, rand_range]
142
+ end
143
+ end
144
+
145
+ def process_str(exp)
146
+ mutate_node [:str, exp.shift]
147
+ end
148
+
149
+ def mutate_str(node)
150
+ [:str, rand_string]
151
+ end
152
+
153
+ def process_if(exp)
154
+ mutate_node [:if, process(exp.shift), process(exp.shift), process(exp.shift)]
155
+ end
156
+
157
+ def mutate_if(node)
158
+ [:if, node[1], node[3], node[2]]
159
+ end
160
+
161
+ def process_true(exp)
162
+ mutate_node [:true]
163
+ end
164
+
165
+ def mutate_true(node)
166
+ [:false]
167
+ end
168
+
169
+ def process_false(exp)
170
+ mutate_node [:false]
171
+ end
172
+
173
+ def mutate_false(node)
174
+ [:true]
175
+ end
176
+
177
+ def process_while(exp)
178
+ cond, body, head_controlled = grab_conditional_loop_parts(exp)
179
+ mutate_node [:while, cond, body, head_controlled]
180
+ end
181
+
182
+ def mutate_while(node)
183
+ [:until, node[1], node[2], node[3]]
184
+ end
185
+
186
+ def process_until(exp)
187
+ cond, body, head_controlled = grab_conditional_loop_parts(exp)
188
+ mutate_node [:until, cond, body, head_controlled]
189
+ end
190
+
191
+ def mutate_until(node)
192
+ [:while, node[1], node[2], node[3]]
193
+ end
194
+
195
+ def mutate_node(node)
196
+ raise UnsupportedNodeError unless respond_to? "mutate_#{node.first}"
197
+ increment_node_count node
198
+ if should_heckle? node
199
+ increment_mutation_count node
200
+ return send("mutate_#{node.first}", node)
201
+ else
202
+ node
203
+ end
204
+ end
205
+
206
+ ############################################################
207
+ ### Tree operations
208
+
209
+ def walk_and_push(node)
210
+ return unless node.respond_to? :each
211
+ return if node.is_a? String
212
+ node.each { |child| walk_and_push(child) }
213
+ if MUTATABLE_NODES.include? node.first
214
+ @mutatees[node.first.to_sym].push(node)
215
+ mutation_count[node] = 0
216
+ end
217
+ end
218
+
219
+ def grab_mutatees
220
+ walk_and_push(current_tree)
221
+ end
222
+
223
+ def current_tree
224
+ ParseTree.translate(klass_name.to_class, method_name)
225
+ end
226
+
227
+ def reset
228
+ reset_tree
229
+ reset_mutatees
230
+ reset_mutation_count
231
+ end
232
+
233
+ def reset_tree
234
+ return unless original_tree != current_tree
235
+ @mutated = false
236
+
237
+ klass = klass_name.to_class
238
+
239
+ self.count += 1
240
+ new_name = "#{method_name}_#{count}"
241
+ klass.send :undef_method, new_name rescue nil
242
+ klass.send :alias_method, new_name, method_name
243
+ klass.send :alias_method, method_name, "#{method_name}_1"
244
+ end
245
+
246
+ def reset_mutatees
247
+ @mutatees = @original_mutatees.deep_clone
248
+ end
249
+
250
+ def reset_mutation_count
251
+ mutation_count.each {|k,v| mutation_count[k] = 0}
252
+ end
253
+
254
+ def reset_node_count
255
+ node_count.each {|k,v| node_count[k] = 0}
256
+ end
257
+
258
+ def increment_node_count(node)
259
+ if node_count[node].nil?
260
+ node_count[node] = 1
261
+ else
262
+ node_count[node] += 1
263
+ end
264
+ end
265
+
266
+ def increment_mutation_count(node)
267
+ # So we don't re-mutate this later if the tree is reset
268
+ mutation_count[node] += 1
269
+ @mutatees[node.first].delete_at(@mutatees[node.first].index(node))
270
+ @mutated = true
271
+ end
272
+
273
+ ############################################################
274
+ ### Convenience methods
275
+
276
+ def should_heckle?(exp)
277
+ return false unless method == method_name
278
+ mutation_count[exp] = 0 if mutation_count[exp].nil?
279
+ return false if node_count[exp] <= mutation_count[exp]
280
+ mutatees[exp.first.to_sym].include?(exp) && !already_mutated?
281
+ end
282
+
283
+ def grab_conditional_loop_parts(exp)
284
+ cond = process(exp.shift)
285
+ body = process(exp.shift)
286
+ head_controlled = exp.shift
287
+ return cond, body, head_controlled
288
+ end
289
+
290
+ def already_mutated?
291
+ @mutated
292
+ end
293
+
294
+ def mutations_left
295
+ sum = 0
296
+ @mutatees.each {|mut| sum += mut.last.size }
297
+ sum
298
+ end
299
+
300
+ def current_code
301
+ RubyToRuby.translate(klass_name.to_class, method_name)
302
+ end
303
+
304
+ def rand_number
305
+ (rand(10) + 1)*((-1)**rand(2))
306
+ end
307
+
308
+ def rand_string
309
+ size = rand(100)
310
+ str = ""
311
+ size.times { str << rand(126).chr }
312
+ str
313
+ end
314
+
315
+ def rand_symbol
316
+ letters = ('a'..'z').to_a + ('A'..'Z').to_a
317
+ str = ""
318
+ rand(100).times { str << letters[rand(letters.size)] }
319
+ :"#{str}"
320
+ end
321
+
322
+ def rand_range
323
+ min = rand(50)
324
+ max = min + rand(50)
325
+ min..max
326
+ end
327
+
328
+ # silence_stream taken from Rails ActiveSupport reporting.rb
329
+
330
+ # Silences any stream for the duration of the block.
331
+ #
332
+ # silence_stream(STDOUT) do
333
+ # puts 'This will never be seen'
334
+ # end
335
+ #
336
+ # puts 'But this will'
337
+ def silence_stream(stream)
338
+ unless @@debug
339
+ old_stream = stream.dup
340
+ stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
341
+ stream.sync = true
342
+ end
343
+ yield
344
+ ensure
345
+ stream.reopen(old_stream) unless @@debug
346
+ end
347
+
348
+ end
349
+ end