heckle 1.0.0 → 1.1.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.
@@ -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