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.
- 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
|