heckle 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +16 -0
- data/Manifest.txt +8 -1
- data/README.txt +4 -4
- data/Rakefile +5 -1
- data/bin/heckle +30 -8
- data/lib/heckle.rb +3 -86
- data/lib/heckle/base.rb +349 -0
- data/lib/heckle/reporter.rb +43 -0
- data/lib/test_unit_heckler.rb +45 -0
- data/sample/Rakefile +16 -0
- data/sample/changes.log +91 -0
- data/sample/lib/heckled.rb +63 -0
- data/sample/test/test_heckled.rb +19 -0
- data/test/fixtures/heckled.rb +103 -0
- data/test/test_heckle.rb +642 -17
- metadata +24 -8
data/History.txt
CHANGED
@@ -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
|
data/Manifest.txt
CHANGED
@@ -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
|
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
|
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
|
1
|
+
#!/usr/local/bin/ruby
|
2
2
|
|
3
|
-
|
4
|
-
require '
|
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
|
11
|
-
|
28
|
+
unless impl then
|
29
|
+
puts opts
|
30
|
+
exit 1
|
12
31
|
end
|
13
32
|
|
14
|
-
|
15
|
-
|
33
|
+
if meth
|
34
|
+
TestUnitHeckler.new(impl, meth).validate
|
35
|
+
else
|
36
|
+
TestUnitHeckler.validate(impl)
|
37
|
+
end
|
data/lib/heckle.rb
CHANGED
@@ -1,89 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'parse_tree'
|
3
3
|
require 'ruby2ruby'
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
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'
|
data/lib/heckle/base.rb
ADDED
@@ -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
|